在线教育的核心资产是课程视频,而视频一旦被人拿到原始地址批量下载、拼车共享,付费体系就形同虚设。很多团队初期只用 Referer 防盗链或在 URL 里拼一个固定密钥,结果很快被抓包绕过。可靠的视频保护不是单一开关,而是"播放凭证签发、时效控制、续期、并发与异常行为约束"串起来的一套机制。本文拆解生产可用的播放鉴权方案,以及每一步最容易被忽略的坑。
先排除两种常见的"伪防盗链":
<video src> 引用你的地址,挡不住蓄意下载;正确思路是把"能不能看、看到什么时候、在几台设备看"都收敛到有时效、可追溯、和用户身份绑定的播放凭证上,原始视频地址不直接暴露给前端。
播放地址由服务端按需签发,包含对象标识、用户、过期时间,并对这些字段做签名。客户端拿到的是一个"过期即作废、篡改即失效"的临时地址。
一个典型的签名地址形如:
https://media.example.com/play/course-88/lesson-3.m3u8 ?uid=10086&exp=1725960000&rnd=9f3a&sig=8c1d...
服务端校验逻辑:
function verifySignedUrl(query, secret) {
const { uid, exp, rnd, sig } = query;
if (Date.now() / 1000 > Number(exp)) return { ok: false, reason: "EXPIRED" };
const expect = hmac(secret, `${uid}.${exp}.${rnd}`); // 与签发端同一算法
if (!timingSafeEqual(expect, sig)) return { ok: false, reason: "BAD_SIGN" };
return { ok: true, uid };
}关键点:
直接把登录 Token 的有效期设成视频时长并不合适——登录态要长期保持,播放凭证却应当短命。推荐两层结构:
POST /course/play-ticket Authorization: Bearer <loginToken>
# 响应: { "ticket": "pt_xxx", "expiresIn": 300, "playUrl": "https://..." }这样即使播放地址泄露,攻击窗口也只有几分钟;而用户正常观看一节课通常超过票据有效期,于是需要续期机制。
视频不能看到一半突然鉴权失败。续期要做到"用户无感知、且只有真实在看的人能续":
防盗链防的是地址外泄,并发限制防的是"一个会员多人同时用"。常见做法:
// 会话抢占:只保留最新 maxConcurrency 路
async function startPlay(uid, deviceId) {
const sessions = await redis.lrange(`play:${uid}`, 0, -1);
if (sessions.length >= MAX_CONCURRENCY) {
await evictOldest(uid); // 通知最旧一路"账号在别处播放"
}
await redis.lpush(`play:${uid}`, JSON.stringify({ deviceId, at: Date.now() }));
}注意并发判断要放在服务端、用原子操作(Lua 脚本或事务)完成,不能依赖前端自觉;同时要给"同一用户换设备、网络切换"留容错,避免正常用户频繁被误踢。
签名和并发解决"身份与数量",行为分析解决"看起来合法但不正常":
这些信号不一定要实时封号,可以先打分、分级处置:低风险只要求重新验证,高风险临时冻结播放并人工复核,兼顾安全与正常用户体验。
落地顺序建议分三步:第一步先上"短期签名 URL + HMAC + 分片签名",用最小成本挡住绝大多数地址外泄;第二步引入"登录态/播放票据两层凭证 + 无感续期 + 加密密钥接口",把攻击窗口压到分钟级;第三步再叠加"设备与并发会话 + 行为风控"。存储上,活跃会话适合放在 Redis 并设置与心跳匹配的 TTL;签名与续期逻辑收敛在网关或媒体鉴权服务,便于统一轮换密钥。务必保留完整的签发、续期、抢占、拒绝日志,让每一次播放都能追溯到账号与设备——安全机制不仅要拦得住,还要事后查得到。
视频防盗链的本质,是让"地址"变得短期、和人绑定、可被追溯,让"播放"变得有数量上限、有行为合理性校验。与其追求一个绝对无法破解的方案,不如用分层机制把批量泄露和账号共享的成本抬到远高于课程本身的价值——对绝大多数在线教育场景,这就是综合代价最低、也最稳妥的防护。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。