视频云平台的安全实践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 永不向前端输出 appSecret1. 签名每一请求独立的 time nonce现行规范签名原始串固定顺序拼接后 MD5UTF-832 位小写十六进制time:{unix秒},nonce:{32位随机},appSecret:{密钥}importcryptofromnode:crypto;import{randomUUID}fromnode:crypto;functioncalcSign(time,nonce,appSecret){constrawtime:${time},nonce:${nonce},appSecret:${appSecret};returncrypto.createHash(md5).update(raw,utf8).digest(hex);}functionbuildRequestBody(appId,appSecret,params{}){consttimeMath.floor(Date.now()/1000);constnoncerandomUUID();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 漂移 → 批量签名失败。监控机器时钟容器部署尤其要查。官方标准案例time1706511734, 给定 nonce/secretsign 应为fd37b62889e4757c58b8f3bf05fb9976——上线前用单测锁死算法别靠肉眼对。2. accessToken缓存 主动刷新别「每点击一次拉一次」asyncfunctiongetAdminToken(appId,appSecret,cache){constcachedawaitcache.get(lc:admin:token);if(cached)returncached;constdataawaitcallOpenApi(accessToken,appId,appSecret,{});// data.accessToken, data.expireTime剩余有效秒数constttlMath.max(data.expireTime-300,60);// 提前 5 分钟过期awaitcache.set(lc:admin:token,data.accessToken,EX,ttl);returndata.accessToken;}文档要点管理员accessToken 有效期约 3 天遇TK1002表示 token 失效清缓存重拉使用超过 2 天未满 3 天再次请求时会发新 token新旧在各自生命周期内仍可用——刷新时不要假设旧 token 立刻作废但业务层应统一切新值。踩坑 C大促日每个用户打开监控页都调accessToken→ 配额浪费 延迟飙升。令牌是服务级缓存不是用户级。子账号 token生产建议按门店/租户开子账号最小权限如仅Live、RecordReplay、Ptz对应资源cam:序列号:通道号泄露面小于管理员 token。3. 统一网关业务 API 与 OpenAPI 之间加一道asyncfunctioncallOpenApi(method,appId,appSecret,params){constbodybuildRequestBody(appId,appSecret,params);constresawaitfetch(https://openapi.lechange.cn/openapi/${method},{method:POST,headers:{Content-Type:application/json},body:JSON.stringify(body),});constjsonawaitres.json();const{code,msg,data}json.result??{};if(codeTK1002){awaittokenCache.del(lc:admin:token);thrownewTokenExpiredError(msg);}if(code!0)thrownewPlatformError(code,msg);returndata;}对外只暴露POST /api/v1/stores/{id}/live内部再bindDeviceLive。前端永远看不到appSecret、管理员accessToken、完整 m3u8 长期 URL。4. 敏感参数加密encryptCode绑定设备bindDevice的code安全码/密码可明文传高安全场景用encryptCodecode与encryptCode同时传时以 code 为准encryptCode Base64(AES256_CBC(code, key[], iv[])) 算法AES/CBC/PKCS5Padding keyPBKDF2WithHmacSHA256(deviceId, MD5(appSecret))迭代 1200 次256 位 ivMD5(appSecret)日志与工单系统禁止打印明文 code批量绑定时从 KMS 读 appSecret内存里算 encryptCode 后立即丢弃明文。踩坑 D把 encryptCode 算法写在前端「方便实施」——等于把推导 key 的材料暴露给反编译。加密只在服务端。5. 播放层短效 ticket防裸链bindDeviceLive/createDeviceRecordHls返回的 HLS 地址有时效文档亦提醒直播对外公开后他人可直接观看。我们的策略asyncfunctioncreatePlayTicket(userId,deviceId,channelId){awaitassertUserCanView(userId,deviceId);constliveawaitissueWallStream(adminToken,deviceId,channelId);constticketsignJwt({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. 设备侧加密encryptModelistDeviceDetailsByPage同步字段里可见设备encryptMode非 0 表示开启音视频加密。播放/抓图/云转存等场景需按文档传递加密后的password算法与encryptCode同族绑定 deviceId appSecret。忽略此字段会出现「接口成功但播放器 silent fail」——我们排障表单独列一行。7. 审计、限流与重放防护业务层平台 sign 防的是对乐橙网关的重放对你自家 BFF仍要补// 示例同一用户 1 秒内只允许 1 次云台指令asyncfunctionptzWithRateLimit(userId,deviceId,op){constkeyptz:${userId}:${deviceId};if(awaitredis.exists(key))thrownewTooManyRequestsError();awaitredis.set(key,1,EX,1);awaitcallOpenApi(controlMovePTZ,appId,appSecret,{token:adminToken,deviceId,channelId:0,operation:String(op),duration:500,});}控制项建议直播 ticket5–15 分钟 JWT 单次绑定 session云台/绑定用户 设备维度限流审计谁、何时、哪路、何种操作日志脱敏 appSecret、code、完整 URL8. 安全验收清单□ 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 敏感操作仍走服务端。常见错误码安全相关码含义处理SN1005nonce 5 分钟内重复每请求新 UUIDTK1002token 失效清缓存重取 accessTokensign 相关串顺序/大小写/时钟对照开发规范单测不要做的「省事」把 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、绑定设备等章节请使用现行协议勿引用旧版本协议栏目。以上为项目实践记录生产以最新文档为准。