PAYATHON 2026

HTML5 付费视频播放器如何安全使用外部托管文件?

支付老李

结论

视频可以继续托管在客户自己的服务器上,但该服务器或前置 CDN 必须支持访问鉴权。播放器无法保护公开的视频 URL,也无法阻止已有播放权限的用户通过浏览器查看媒体请求。

可行的设计流程是:

  1. 用户付款后,后端确认其观看权限。
  2. 后端签发有效期较短、与具体资源绑定的播放凭证。
  3. 客户的视频服务器或 CDN 验证凭证。
  4. 验证通过后,服务器才返回视频文件或流媒体分片。

如果外部服务器只能提供永久公开的 URL,也无法增加鉴权逻辑,那么仅靠 HTML5 播放器无法实现安全的付费访问。

为什么不能依靠播放器隐藏地址

HTML5 <video> 最终必须向服务器请求媒体数据。无论 URL 是直接写在 HTML 中、通过 JavaScript 动态设置,还是经过混淆,用户都能通过浏览器开发者工具、代理软件或网络日志看到请求。

例如,下面的写法没有保护作用:

<video controls src="https://media.example.com/movie.mp4"></video>

以下手段同样不能承担付费鉴权:

  • 禁用右键菜单
  • 隐藏下载按钮
  • 混淆 JavaScript
  • 检查 Referer
  • 限制 CORS
  • 将真实 URL 放进前端加密
  • 用播放器页面遮挡视频元素

CORS 主要限制其他网页中的 JavaScript 读取跨域响应,并不会阻止用户直接访问媒体 URL。Referer 也可能缺失或被伪造,最多只能用于辅助风控。

推荐的访问流程

典型流程如下:

用户付款
   ↓
业务后端确认订单和观看权限
   ↓
签发短时播放令牌或签名 URL
   ↓
播放器取得播放地址
   ↓
外部视频服务器/CDN验证签名
   ↓
返回 MP4、HLS 或 DASH 内容

播放令牌至少应包含:

  • 视频资源标识
  • 用户或会话标识
  • 过期时间
  • 随机数或令牌 ID
  • 签发方
  • 防篡改签名

不要让前端判断支付是否成功。付款回调、订单状态和播放授权都应由可信后端校验。

方案一:短时签名 URL

如果客户服务器支持签名校验,可以在每次播放时生成短时 URL:

https://media.customer.com/videos/123/master.m3u8
    ?expires=1788000000
    &token=...

下面是一个简化的 Node.js 签名示例:

import crypto from "node:crypto";

function createSignedUrl({ baseUrl, resourcePath, userId, expires, secret }) {
  const payload = `${resourcePath}\n${userId}\n${expires}`;

  const token = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("base64url");

  const url = new URL(resourcePath, baseUrl);
  url.searchParams.set("uid", userId);
  url.searchParams.set("expires", String(expires));
  url.searchParams.set("token", token);

  return url.toString();
}

外部视频服务器收到请求后,需要重新计算签名:

import crypto from "node:crypto";

function verifyRequest({ resourcePath, userId, expires, token, secret }) {
  if (!Number.isInteger(expires) || expires < Math.floor(Date.now() / 1000)) {
    return false;
  }

  const payload = `${resourcePath}\n${userId}\n${expires}`;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest();

  let received;

  try {
    received = Buffer.from(token, "base64url");
  } catch {
    return false;
  }

  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected)
  );
}

这段代码只展示了核心逻辑。生产环境还要限制签名适用的资源路径、令牌有效期和使用范围,并妥善保管密钥。

签名 URL 在有效期内仍可能被复制,因此有效期应尽量缩短。必要时还可以绑定登录会话、设备或 IP。不过,绑定 IP 可能影响使用移动网络、代理或频繁切换网络的用户。

HLS 和 DASH 通常会请求一个清单文件和大量媒体分片。如果每个分片都要携带查询参数,管理会比较麻烦。对于这种场景,支持签名 Cookie 的 CDN 或媒体服务器通常更合适。

授权成功后,后端可以设置受保护的 Cookie:

Set-Cookie: playback_token=...; Path=/protected/video/123/; Secure; HttpOnly; SameSite=None

播放器加载:

<video
  controls
  src="https://media.customer.com/protected/video/123/master.m3u8">
</video>

媒体服务器统一验证清单、加密密钥和所有分片请求中的 Cookie。

跨站嵌入时,Cookie 会受到浏览器第三方 Cookie 策略的影响,不能假定所有浏览器都允许这种方式。遇到这类部署结构时,可以改用同站域名、反向代理,或者在 CDN 支持的范围内使用请求头和签名 URL。

方案三:授权服务配合反向代理

如果客户的存储服务器不方便实现鉴权,可以在其前面部署 Nginx、应用网关或 CDN。网关负责验证令牌,源站只接受来自网关的访问。

以 Nginx 的 auth_request 为例:

location /protected-video/ {
    auth_request /_verify_playback;
    proxy_pass https://private-media-origin;
    proxy_set_header Host private-media-origin;
}

location = /_verify_playback {
    internal;
    proxy_pass https://auth.example.com/api/playback/verify;
    proxy_pass_request_body off;
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header Authorization $http_authorization;
}

授权服务返回 2xx 时允许播放,返回 401 或 403 时拒绝请求。

源站也必须限制公网访问。否则,用户可以绕过网关直接访问源站地址,鉴权便失去了作用。常见做法包括:

  • 源站仅向 CDN 或代理服务器开放
  • 使用对象存储私有桶
  • 通过源站访问控制策略限制来源
  • CDN 与源站之间使用独立凭证或双向 TLS

MP4 直传时要支持 Range 请求

浏览器播放 MP4 时通常会发送字节范围请求:

Range: bytes=1048576-

媒体服务器需要正确返回:

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 1048576-2097151/50000000

鉴权不能只检查第一次请求,后续的 Range 请求也必须验证令牌,否则可能留下绕过入口。令牌有效期还要覆盖正常播放过程,或者允许播放器在令牌过期前刷新播放地址。

HLS 或 DASH 的额外要求

使用 HLS 或 DASH 时,不能只保护入口清单。以下资源都要经过鉴权:

  • 主清单
  • 子清单
  • 视频和音频分片
  • 字幕文件
  • 加密密钥
  • 初始化分片

如果只保护 master.m3u8,而 .ts、.m4s 或密钥 URL 仍然公开,用户依旧可以直接下载内容。

播放器可以通过以下方式获取授权地址:

async function startPlayback(videoId) {
  const response = await fetch(`/api/videos/${videoId}/playback`, {
    method: "POST",
    credentials: "include"
  });

  if (!response.ok) {
    throw new Error("Playback authorization failed");
  }

  const { playbackUrl } = await response.json();

  const video = document.querySelector("video");
  video.src = playbackUrl;
  await video.play();
}

/api/videos/:id/playback 必须由后端检查登录状态、订单状态和观看权限,不能仅根据前端传来的”已付款”字段签发 URL。

外部托管平台需要提供什么

如果要支持第三方客户托管视频,可以规定以下几种接入模式:

  1. 客户使用支持签名 URL 或签名 Cookie 的 CDN。
  2. 客户在自己的服务器上安装鉴权中间件。
  3. 客户允许你的授权服务通过回调接口参与鉴权。
  4. 客户将私有源站接入你提供的代理或 CDN。
  5. 客户使用兼容私有对象存储的临时访问地址。

播放器产品除了提供嵌入脚本,还应提供鉴权协议、签名算法、密钥轮换方式、服务器端 SDK 和部署说明。

不要把全局共享密钥放进播放器 JavaScript。浏览器中的密钥最终都能被用户读取,签名操作必须在服务器端完成。

如果要求更强的内容保护

短时签名 URL 解决的是未授权用户直接访问源文件的问题,但无法彻底阻止已授权用户录制或保存播放内容。只要浏览器能够解码并显示视频,内容就有被捕获的可能。

对于高价值内容,可以考虑使用基于 Encrypted Media Extensions 的 DRM,例如:

  • Widevine
  • FairPlay Streaming
  • PlayReady

DRM 通常需要加密打包、许可证服务器,以及支持相应 DRM 的播放器和浏览器环境,部署成本和复杂度都更高。即使使用 DRM,也无法完全防止屏幕录制,但可以明显增加直接下载和离线解密的难度。

实施时的注意事项

  • 所有播放页面和媒体请求都应使用 HTTPS。
  • 播放凭证应短时有效,并限定到具体视频。
  • 签名应使用 HMAC-SHA-256 等成熟算法,避免自行设计加密算法。
  • 密钥只能保存在服务器或受控的密钥管理系统中。
  • 令牌验证应覆盖清单、分片、字幕、密钥和 Range 请求。
  • 源站不应保留可绕过 CDN 或代理的公开入口。
  • 记录令牌签发、访问拒绝和异常并发等审计信息。
  • 可以限制同时播放数量,但要考虑网络重连和多标签页可能造成的误判。
  • 不应在访问日志、分析脚本或第三方错误上报中泄露完整的签名 URL。
  • 配置缓存时,要避免不同用户的授权响应被错误共享。

播放器负责播放,服务器负责授权。只要视频服务器或 CDN 能根据付款后签发的凭证拒绝未授权请求,外部托管就可以实现。如果托管端始终提供公开文件,就没有可靠的方法只靠 HTML5 播放器补救。

备注:内容仅供参考。