10 KiB
10 KiB
OIDC JWT Claims 设计
日期:2026-04-21
1. 文档目的
这份文档用于完成 M2 中的两条任务:
设计 JWT claims确认 iss/sub/sid/provider/roles 字段
目标是把当前 Node 后端只包含 sub + ver 的轻量 JWT,升级为一份既兼容 Axum Bearer 鉴权、又可用于 SpacetimeDB 身份透传的 OIDC 风格 claims 设计,并固定:
- 必须出现的标准字段与扩展字段
- 哪些字段属于 access token,哪些不应该塞进 JWT
- Axum、
platform-auth、module-auth、SpacetimeDB各自如何使用这些 claims - 与当前 Node token 口径的映射方式
2. 当前基线
当前 Node token 实现位于:
server-node/src/auth/token.tsserver-node/src/middleware/auth.tsserver-node/src/types/express.d.ts
当前 Node access token 口径:
- Header:
alg = HS256typ = JWT
- 标准字段:
sub = userIdiss = config.jwtIssueriatexp
- 自定义字段:
ver = tokenVersion
当前主要问题:
- claims 信息过薄,不足以直接支撑
SpacetimeDB侧的身份上下文判断 sid/provider/roles等字段尚未冻结,后续platform-auth与module-auth实现没有稳定目标- 当前 Express
request.auth里也只有userId与tokenVersion
3. 设计目标
新的 access token 必须同时满足:
- Axum 可直接做 Bearer 校验与最小授权判断
SpacetimeDB可识别发行者与稳定主体身份- 后续
module-auth可基于sid/provider/roles做会话与能力判定 - 前端无需解析大量敏感信息
- claims 尺寸保持克制,不把会话明细或风控状态塞进 JWT
4. token 分层
当前阶段固定只设计两类 token:
4.1 Access Token
用途:
Authorization: Bearer <token>- Axum 路由鉴权
SpacetimeDB身份透传
特点:
- 短期有效
- 带完整最小 claims
- 可在服务间透传
4.2 Refresh Session Cookie
用途:
- 浏览器长期登录续期
- 轮换 access token
特点:
- 不走 JWT
- 对应
refresh_session表 - 不通过
SpacetimeDB透传
结论:
- 只有 access token 需要本设计文档中的 claims
- refresh cookie 不追加 JWT claims 设计复杂度
5. Claims 总体设计
当前阶段固定采用:
5.1 标准字段
| 字段 | 必填 | 说明 |
|---|---|---|
iss |
是 | 发行者,固定为 Axum 鉴权发行者。 |
sub |
是 | 稳定用户 ID,对应 user_account.user_id。 |
aud |
否 | 当前阶段可暂不强制;若启用,固定为 genarrative-api。 |
iat |
是 | 签发时间。 |
exp |
是 | 过期时间。 |
nbf |
否 | 当前阶段不强制。 |
jti |
否 | 当前阶段不强制;若后续需要细粒度吊销,再单独扩展。 |
5.2 扩展字段
| 字段 | 必填 | 说明 |
|---|---|---|
sid |
是 | 会话 ID,对应 refresh_session.session_id 或当前 access token 所属会话。 |
provider |
是 | 登录来源,固定为 password、phone、wechat。 |
roles |
是 | 角色列表;当前默认至少包含 user。 |
ver |
是 | 用户 token 版本,对应 user_account.token_version。 |
phone_verified |
是 | 是否已完成手机号验证。 |
binding_status |
是 | 账号绑定状态,固定为 active、pending_bind_phone。 |
display_name |
否 | 当前展示名快照,用于少量上游日志/观测;不是授权依据。 |
6. 关键字段定义
6.1 iss
固定要求:
- 必填
- 必须可稳定配置
- 必须作为 Axum 与
SpacetimeDB共同信任的发行者标识
当前阶段建议:
- 本地开发默认:
https://auth.genarrative.local - 测试/生产环境由
GENARRATIVE_JWT_ISSUER或等价配置显式提供
说明:
- 不继续沿用 Node 当前的
genarrative-server-node这种非 URL 风格字符串。 - 既然目标是 OIDC 风格 claims,
iss应该升级为稳定 issuer 标识。
6.2 sub
固定要求:
- 必填
- 值为稳定用户 ID
- 不能使用手机号、微信 openid 或用户名作为
sub
来源:
user_account.user_id
6.3 sid
固定要求:
- 必填
- 表示当前 access token 所属的会话 ID
- 用于会话吊销、全端登出和会话列表关联
来源:
refresh_session.session_id
说明:
- 即便当前 access token 是由 refresh cookie 刷新得到,
sid仍固定指向同一会话 ledger。 sid是会话真相的索引,不是一次 access token 的唯一 ID。
6.4 provider
固定枚举:
passwordphonewechat
来源:
- 优先来源于当前会话完成登录时的主 provider
- 与
auth_identity.provider、user_account.login_provider保持兼容
6.5 roles
固定要求:
- 必填
- 类型为字符串数组
- 当前阶段至少包含
user
当前阶段角色基线:
user
说明:
- 当前阶段不预支
admin/moderator/devops等角色体系 - 但字段必须现在就冻结,避免后续 breaking change
6.6 ver
固定要求:
- 必填
- 表示当前用户 token 版本
- 用于全局登录失效控制
来源:
user_account.token_version
兼容说明:
- 继续兼容当前 Node
ver设计 - Axum 校验时必须比对数据库中的最新
token_version
6.7 phone_verified
固定要求:
- 必填
true表示账号已具备已验证手机号false表示例如微信待绑手机号场景
来源:
user_account.phone_verified_at != null
6.8 binding_status
固定枚举:
activepending_bind_phone
来源:
user_account.account_status的鉴权视图
说明:
- 它不是完整账号状态枚举,只是鉴权流程需要的最小绑定状态快照
- 禁止把大量内部状态枚举直接透传成 JWT claim
6.9 display_name
固定规则:
- 可选
- 仅作为展示快照
- 不能用于授权或数据归属判断
说明:
- 即使写入,也必须把它视为弱一致字段
- 后续若变更显示名,不要求立即使所有 JWT 失效
7. 不进入 JWT 的字段
以下内容当前阶段禁止进入 access token:
- 原始手机号
- 手机号脱敏值
- 微信 openid / unionid
- refresh token hash
- 风控状态、captcha 状态、封禁剩余时间
- 完整用户资料对象
- 审计日志、设备列表、IP、UA
原因:
- 这些字段要么敏感,要么高频变动,要么不适合做 claims
- 统一由数据库真相或接口读取承担
8. 推荐 payload 形态
{
"iss": "https://auth.genarrative.local",
"sub": "usr_123",
"sid": "sess_456",
"provider": "wechat",
"roles": ["user"],
"ver": 3,
"phone_verified": false,
"binding_status": "pending_bind_phone",
"display_name": "微信旅人",
"iat": 1713657600,
"exp": 1713664800
}
说明:
- 示例只表达字段形态,不锁死具体编码库细节
iat/exp由签发库按标准时间戳表达
9. Axum / platform-auth / module-auth / SpacetimeDB 的使用边界
9.1 platform-auth
负责:
- 签发 access token
- 校验
iss/exp/sub/sid/provider/roles/ver - 解析 claims 为统一 Rust 结构
9.2 module-auth
负责:
- 提供 claims 所依赖的用户、会话、绑定状态真相
- 定义
provider、binding_status、token_version的领域语义
9.3 api-server
负责:
- 从 Bearer token 中提取 claims
- 做路由级鉴权与用户状态校验
- 把最小身份上下文透传给
SpacetimeDB client
9.4 SpacetimeDB
负责:
- 基于受信任 issuer 识别主体身份
- 在 reducer / view 上下文中读取稳定主体身份
当前阶段约束:
SpacetimeDB身份判定以iss + sub为核心sid/provider/roles主要服务于应用层与后续模块授权,不要求第一版在 reducer 中过度使用
10. 与当前 Node token 的映射关系
| 当前 Node | 新 claims | 迁移规则 |
|---|---|---|
sub = userId |
sub |
原样保留,但语义冻结为稳定用户 ID。 |
iss = jwtIssuer |
iss |
继续保留,但升级为 OIDC 风格 issuer 标识。 |
ver = tokenVersion |
ver |
原样保留。 |
| 无 | sid |
新增,绑定会话主键。 |
| 无 | provider |
新增,绑定本次登录 provider。 |
| 无 | roles |
新增,当前至少固定为 ["user"]。 |
| 无 | phone_verified |
新增,表达手机号验证状态。 |
| 无 | binding_status |
新增,表达待绑手机/已激活状态。 |
| 无 | display_name |
可选新增。 |
11. Express / Axum 请求上下文映射
当前 Node Express.Request.auth 只有:
userIdtokenVersion
Rust 侧建议升级为统一 claims 结构,例如:
user_idsession_idproviderrolestoken_versionphone_verifiedbinding_status
说明:
api-server不再只传userId- 但 handler 仍应优先依赖最小必要字段,避免所有路由都耦合完整 claims
12. 不允许的设计漂移
后续实现时禁止出现以下情况:
- 继续只保留
sub + ver,却声称已完成SpacetimeDB身份透传设计 - 把手机号、openid、unionid 等敏感信息直接塞进 JWT
- 把
sid设计成一次 access token 的随机 ID,而不是会话 ID - 把
roles省略,等未来再补,导致后续 claims 结构 breaking change - 在
SpacetimeDB场景里信任客户端传入的 user id,而不是受信任 JWT 的sub
13. 本任务完成定义
当以下条件满足时,JWT claims 设计任务视为完成:
iss/sub/sid/provider/roles已明确冻结- access token 与 refresh session 的职责边界已切开
- Axum、
platform-auth、module-auth、SpacetimeDB的使用边界已明确 - 后续可以直接按这份文档实现签发、校验与身份透传
14. 依据文件
server-node/src/auth/token.tsserver-node/src/middleware/auth.tsserver-node/src/auth/refreshSessionCookie.tsserver-node/src/config.tsserver-node/src/types/express.d.tspackages/shared/src/contracts/auth.tsdocs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.mddocs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.mddocs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md