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