AccessToken2 语义与权限位
权限位、频道名/uid 绑定、exp 与 pexp 分离、通配 Token、续期宽限,以及我们与声网的差异。
这一页讲 Token 本身的语义:里面有什么、谁签名、什么时候失效、越权会怎样。签发/续期/校验的接口调用方式见 Token 鉴权;两者的关系是「语义 vs 用法」。
一句话模型
Token = 项目 App ID + 频道名 + uid + 角色权限集 + 过期时间 的签名快照,用项目的 App Certificate 做 HMAC-SHA256。 服务端与 SDK 都把它当不透明字符串:不解密、不拼接、不缓存解析结果做鉴权判断。
线格式
nrt1.<base64url(payload JSON)>.<base64url(HMAC-SHA256(appCertificate, "nrt1.<base64url(payload)>"))>| 段 | 内容 | 说明 |
|---|---|---|
| 前缀 | nrt1 | 格式版本;将来若改结构会升到 nrt2,SDK 与网关同时兼容一段时间 |
| payload | base64url(JSON) | 字段见下表;不是加密,任何人都能解出来,所以不要往里塞敏感信息 |
| 签名 | base64url(HMAC-SHA256) | 用 appCertificate 签名;校验用常量时间比较,防时序侧信道 |
payload 字段
以下字段由 控制面组件 定义,并镜像到 接口定义:
| 字段 | 类型 | 含义 | |
|---|---|---|---|
v | number | 版本,当前 1 | |
app | string | 项目 App ID(32 位十六进制) | |
ch | string | 频道名;* 表示通配(任意频道) | |
uid | number | 数字 uid,1..2147483647 | |
role | host \ | audience | 决定默认权限集 |
priv | string[] | 实际权限位(见下表),是授权判断的唯一依据 | |
iat | number | 签发时间(epoch 秒) | |
exp | number | Token 生命周期到期(epoch 秒) | |
pexp | number | 权限到期(epoch 秒),恒 ≤ exp | |
salt | number | 随机数,保证相同输入产生不同 Token(防指纹/重放聚类) | |
usr | string? | 身份提示,仅用于审计与日志关联,不参与授权 |
权限位
| 权限位 | 含义 | host | audience |
|---|---|---|---|
join_channel | 允许进入频道 | ✅ | ✅ |
publish_audio | 允许发布音频 | ✅ | — |
publish_video | 允许发布视频 | ✅ | — |
publish_data | 允许发送数据通道消息 | ✅ | — |
subscribe | 允许订阅远端音视频 | ✅ | ✅ |
GET /v1/meta/privileges 返回这张表(按角色),客户端 SDK 与文档读的是同一份数据,避免「文档说 host 能发、实际被拒」。
exp 与 pexp 为什么要分开
声网 AccessToken2 把「Token 有效期」和「权限有效期」拆开,我们保持一致:
exp:Token 字符串本身还能不能被接受(续期窗口由它决定);pexp:里面的权限还能不能用(通常更短,用于「先续期、后收紧权限」)。
三个典型的过期状态因此互不相同:
| 状态 | 判定 | SDK 事件 | 客户端应该做什么 |
|---|---|---|---|
| 权限即将过期 | now > pexp - 30s | token-privilege-will-expire | 调服务端续期,然后 renewToken(newToken) |
| 权限已过期但 Token 未过期 | now > pexp 且 now ≤ exp | token-privilege-did-expire | 立刻续期;期间发布会被拒 |
| Token 已过期 | now > exp | 连接被断开(错误号 -109) | 重新入会 |
服务端也会在权限过期前产生事件码 203(token_expiring)的回调,用于在服务端触发续期逻辑,见 回调事件参考。
通配 Token
ch = "*" 的 Token 可进任意频道,用于本地联调与「一个客户端试多个房间」。生产环境不建议:
- 它把授权范围从「一个频道」放大到「整个项目」;
- 一旦泄露,影响面是全部频道,而不是单个频道;
- 审计时无法从 Token 反推用户本该在哪个频道。
通配 Token 只能通过显式请求 wildcard: true 签发,且 GET /v1/tokens/verify 会在响应里明确标出 wildcard: true。
校验顺序与拒绝原因
verifyToken 的判定顺序是固定的,排障时按顺序排除:
| 顺序 | 拒绝原因(TokenRejection) | 对应 REST 错误码 |
|---|---|---|
| 1 | malformed / bad_prefix | invalid_token |
| 2 | bad_version | invalid_token |
| 3 | invalid_signature | invalid_token |
| 4 | expired | token_expired |
| 5 | not_yet_valid(iat 超前,默认容忍 5 秒时钟偏移) | token_not_yet_valid |
| 6 | channel_mismatch | token_channel_mismatch |
| 7 | uid_mismatch | token_uid_mismatch |
| 8 | privilege_expired / missing_privilege | token_privilege_expired |
签名校验先于任何业务校验:签名不对时不会告诉你频道名对不对,避免被当作探测接口。
uid 与 userId 的映射
Token 里只有数字 uid。IM 用户 id(字符串)通过 POST /v1/uids:resolve 映射,映射关系在签发 Token 时传入 userId 建立。两部分是独立链路:Token 合法但映射缺失时,/v1/uids:resolve 会返回部分结果(resolved / unresolved),不会让你整批失败。
安全实践(Token 相关部分)
appCertificate只放服务端,进密钥管理,不进镜像、不进前端 bundle、不进日志。- Token TTL 默认按项目
tokenTtlSeconds,业务侧建议 1 小时内;长会话用续期而不是长 TTL。 - 不要把
salt或 payload 当业务状态存储——它们会随每次签发变化。 - 轮换
appCertificate会让所有已签发 Token 立即失效,必须在客户端有续期路径的时段内做。 - 权限最小化:观众端只签
audience,不要把 host 权限发给前端然后「靠 UI 限制」。