视频云平台的安全实践:Token 刷新机制、数据加密与防重放攻击设计(乐橙 OpenAPI 实战)
上线第三个月,运维在日志里抓到同一组 bindDeviceLive 参数被 不同 IP 在 30 秒内各打了一次——签名却都通过了。排查发现:我们网关把 带 token 的 OpenAPI 请求体 原样暴露给了 H5,攻击者抓包重放即可换流。更糟的是,早期 Demo 把 appSecret 写进了小程序配置。那天之后,安全评审多了一条红线:密钥、令牌、播放地址,三层必须分开管。
为什么视频云的安全不能「等上线再补」
监控、巡店、透明工地、在线监考——这类产品的「资产」是 实时画面和历史录像,泄露代价远高于普通 CRUD 接口:隐私、合规、品牌,一次裸链转发就可能上热搜。乐橙开放平台在文档 应用开发、开发规范 里反复强调:云云对接,OpenAPI 由 开发者后台 调用,客户端只调自己的 BFF——因为 appId / appSecret 一旦进 App 包,反编译即盗用配额。
对我们这类接乐橙设备的 SaaS,安全可以拆成 四层(均基于现行 OpenAPI,勿引用「旧版本协议、不再维护」栏目):
① 请求鉴权层 time + nonce + sign(防篡改、防简单重放)
② 身份令牌层 accessToken 生命周期(业务权限)
③ 敏感数据层 encryptCode / 设备 encryptMode(验证码与音视频加密)
④ 播放暴露层 直播/录播 URL 短效 + 业务 ticket(防裸链扩散)
很多团队 ① 做了,④ 没做——接口都 200,业主群里仍出现「永久监控链接」。本文按我们踩坑后的 网关实现顺序 写:先 sign 与 token,再加密绑定,最后播放 ticket。
总体架构(推荐)
┌──────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ App/H5 │────▶│ 你的 BFF / 网关 │────▶│ 乐橙 OpenAPI │
│ 会话 Cookie│◀───│ sign / token 缓存 │◀───│ accessToken 等 │
└──────────┘ │ 权限 / 审计 / 限流 │ └──────────────────┘
│ appSecret 仅此处 │
└─────────────────────┘
只拿 playTicket 永不向前端输出 appSecret
1. 签名:每一请求独立的 time + nonce
现行规范:签名原始串 固定顺序拼接后 MD5(UTF-8,32 位小写十六进制):
time:{unix秒},nonce:{32位随机},appSecret:{密钥}
import crypto from 'node:crypto';
import { randomUUID } from 'node:crypto';
function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
function buildRequestBody(appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = randomUUID();
return {
system: {
ver: '1.0',
appId,
sign: calcSign(time, nonce, appSecret),
time,
nonce,
},
id: randomUUID(),
params,
};
}
防重放要点(平台侧已内置,你要配合):
| 机制 | 说明 |
|---|---|
| time 窗口 | 与真实时间误差 ≤5 分钟,否则拒绝 |
| nonce 唯一 | 5 分钟内不可重复,重复报 SN1005 |
| 每次新 id | 请求 body 的 id 每次非空唯一 |
踩坑 A:多实例网关用「内存计数器」生成 nonce → 集群碰撞 SN1005。我们改为 UUID v4,碰撞可忽略。
踩坑 B:服务器 NTP 漂移 → 批量签名失败。监控机器时钟,容器部署尤其要查。
官方标准案例(time=1706511734, 给定 nonce/secret)sign 应为 fd37b62889e4757c58b8f3bf05fb9976——上线前用单测锁死算法,别靠肉眼对。
2. accessToken:缓存 + 主动刷新,别「每点击一次拉一次」
async function getAdminToken(appId, appSecret, cache) {
const cached = await cache.get('lc:admin:token');
if (cached) return cached;
const data = await callOpenApi('accessToken', appId, appSecret, {});
// data.accessToken, data.expireTime(剩余有效秒数)
const ttl = Math.max(data.expireTime - 300, 60); // 提前 5 分钟过期
await cache.set('lc:admin:token', data.accessToken, 'EX', ttl);
return data.accessToken;
}
文档要点:
- 管理员 accessToken 有效期约 3 天;
- 遇
TK1002表示 token 失效,清缓存重拉; - 使用超过 2 天未满 3 天再次请求时,会发新 token,新旧在各自生命周期内仍可用——刷新时不要假设旧 token 立刻作废,但业务层应统一切新值。
踩坑 C:大促日每个用户打开监控页都调 accessToken → 配额浪费 + 延迟飙升。令牌是 服务级 缓存,不是 用户级。
子账号 token:生产建议按门店/租户开子账号,最小权限(如仅 Live、RecordReplay、Ptz 对应资源 cam:序列号:通道号),泄露面小于管理员 token。
3. 统一网关:业务 API 与 OpenAPI 之间加一道
async function callOpenApi(method, appId, appSecret, params) {
const body = buildRequestBody(appId, appSecret, params);
const res = await fetch(`https://openapi.lechange.cn/openapi/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
const { code, msg, data } = json.result ?? {};
if (code === 'TK1002') {
await tokenCache.del('lc:admin:token');
throw new TokenExpiredError(msg);
}
if (code !== '0') throw new PlatformError(code, msg);
return data;
}
对外只暴露 POST /api/v1/stores/{id}/live,内部再 bindDeviceLive。前端 永远看不到 appSecret、管理员 accessToken、完整 m3u8 长期 URL。
4. 敏感参数加密:encryptCode 绑定设备
bindDevice 的 code(安全码/密码)可明文传,高安全场景用 encryptCode(code 与 encryptCode 同时传时 以 code 为准):
encryptCode = Base64(AES256_CBC(code, key[], iv[]))
算法:AES/CBC/PKCS5Padding
key:PBKDF2WithHmacSHA256(deviceId, MD5(appSecret)),迭代 1200 次,256 位
iv:MD5(appSecret)
日志与工单系统 禁止打印明文 code;批量绑定时从 KMS 读 appSecret,内存里算 encryptCode 后立即丢弃明文。
踩坑 D:把 encryptCode 算法写在前端「方便实施」——等于把推导 key 的材料暴露给反编译。加密 只在服务端。
5. 播放层:短效 ticket,防裸链
bindDeviceLive / createDeviceRecordHls 返回的 HLS 地址 有时效,文档亦提醒:直播对外公开后他人可直接观看。我们的策略:
async function createPlayTicket(userId, deviceId, channelId) {
await assertUserCanView(userId, deviceId);
const live = await issueWallStream(adminToken, deviceId, channelId);
const ticket = signJwt({
sub: userId,
deviceId,
channelId,
hls: live.hls,
exp: Math.floor(Date.now() / 1000) + 300, // 5 分钟
}, SERVER_PLAY_SECRET);
auditLog({ userId, deviceId, action: 'live_ticket' });
return { ticket };
}
前端用 ticket 换播放地址;禁止把 m3u8 写进分享链接、数据库永久字段。会话结束,ticket 作废。
6. 审计、限流与重放防护(业务层)
平台 sign 防的是 对乐橙网关的重放;对你 自家 BFF 仍要补:
// 示例:同一用户 1 秒内只允许 1 次云台指令
async function ptzWithRateLimit(userId, deviceId, op) {
const key = `ptz:${userId}:${deviceId}`;
if (await redis.exists(key)) throw new TooManyRequestsError();
await redis.set(key, '1', 'EX', 1);
await callOpenApi('controlMovePTZ', appId, appSecret, {
token: adminToken,
deviceId,
channelId: '0',
operation: String(op),
duration: '500',
});
}
| 控制项 | 建议 |
|---|---|
| 直播 ticket | 5–15 分钟 JWT + 单次绑定 session |
| 云台/绑定 | 用户 + 设备维度限流 |
| 审计 | 谁、何时、哪路、何种操作 |
| 日志 | 脱敏 appSecret、code、完整 URL |
7. 安全验收清单
□ sign 单测与官方案例一致
□ appSecret 仅 KMS/环境变量,仓库无密钥
□ accessToken 服务级缓存 + TK1002 自动刷新
□ 前端/H5/小程序无 OpenAPI 直连
□ bindDevice 生产用 encryptCode 或安全通道传 code
□ 播放 ticket 短效,无永久 m3u8 入库
□ 子账号最小权限(非全局管理员 token 下发客户端)
□ encryptMode≠0 设备已测播放/转存
密钥轮换与事故响应
- appSecret 轮换:控制台更新后,灰度实例滚动重启;旧 secret 保留 24h 窗口防漏网。
- 疑似泄露:立即轮换 secret、作废所有 accessToken 缓存、审计近 7 日异常 IP 与设备操作。
- 直播事故:若 m3u8 已扩散,设备侧重绑或关闭直播计划,而非只改前端。
与 OpenSDK 的边界
文档 标准化 Demo 说明:无后台的 Demo 可能在客户端直调 OpenAPI——仅用于学习。生产必须 BFF + 云云对接;SDK 负责配网、播放、图片解密等端能力,HTTP 敏感操作仍走服务端。
常见错误码(安全相关)
| 码 | 含义 | 处理 |
|---|---|---|
| SN1005 | nonce 5 分钟内重复 | 每请求新 UUID |
| TK1002 | token 失效 | 清缓存重取 accessToken |
| sign 相关 | 串顺序/大小写/时钟 | 对照开发规范单测 |
不要做的「省事」
- 把 sign 算法 port 到前端「减轻服务器压力」
- 把管理员 token 塞进 JWT 给移动端
- 监控墙把 10 路 m3u8 写进 HTML 源码
- 测试环境与生产共用 appSecret
小结
乐橙视频云接入的安全,可以记成 「四不」:
- appSecret 不进客户端
- accessToken 不每次现拉(缓存 + TK1002 刷新)
- sign 不重复使用 nonce(配合 time 窗口防重放)
- 播放 URL 不当永久资产(ticket + 审计)
平台在 开发规范 里提供了 sign、令牌、encryptCode 等机制;能不能扛生产,取决于你的 BFF 是否把密钥、令牌、播放暴露三层拆开。若你正在接 listDeviceDetailsByPage、直播、云录像或 PTZ,建议先把 网关 + token 缓存 + 播放 ticket 搭好,再叠业务功能——比事后补安全便宜一个数量级。
如何开始:在 乐橙开放平台 完成开发者注册并创建应用,于控制台获取 AppId 与密钥;在 在线开发文档 查阅开发规范、应用开发、accessToken、绑定设备等章节(请使用现行协议,勿引用旧版本协议栏目)。以上为项目实践记录,生产以最新文档为准。
330

被折叠的 条评论
为什么被折叠?



