# platform-auth JWT 适配设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于指导 `platform-auth` 首个真实能力落地,目标是完成: 1. `platform-auth` crate 的 JWT claims 结构。 2. access token 的签发与校验适配。 3. `api-server` 的最小 Bearer JWT 校验入口。 这一步只解决“Axum 自身能稳定签发并校验 JWT”的基础问题,不提前把 refresh cookie、短信和微信 OAuth 一起耦合进来。 ## 2. 当前落地范围 本阶段只包含以下实现: 1. `JwtConfig` 2. `AccessTokenClaimsInput` 3. `AccessTokenClaims` 4. `sign_access_token(...)` 5. `verify_access_token(...)` 6. `api-server` 的 Bearer 鉴权中间件 7. `/_internal/auth/claims` 内部调试路由 本阶段明确不包含: 1. refresh cookie 的生成、解析、轮换和吊销。 2. 从 `module-auth` / `SpacetimeDB` 真相表读取 `token_version` 并做在线比对。 3. 短信 provider 与微信 OAuth 平台适配。 4. SpacetimeDB 模块对 Axum 签发 JWT 的消费代码。 ## 3. 设计输入 本实现直接受以下文档约束: 1. [OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](./OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md) 2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) 关键冻结点: 1. `iss/sub/sid/provider/roles/ver/phone_verified/binding_status` 是当前 access token 的固定字段。 2. `sub` 是稳定 `user_id`。 3. `sid` 是会话 ID,不是单次 token ID。 4. `roles` 当前至少包含 `user`。 5. 不允许把手机号、openid、风控状态、refresh token hash 放进 JWT。 ## 4. crate 边界 ### 4.1 `platform-auth` 负责: 1. 组织 JWT 结构。 2. 执行签名与验签。 3. 执行基础 claims 完整性校验。 不负责: 1. 用户是否存在。 2. `token_version` 是否仍是数据库最新值。 3. refresh session 是否已被吊销。 4. provider 之外的业务规则判断。 ### 4.2 `api-server` 负责: 1. 从 `Authorization` 头提取 Bearer token。 2. 调用 `platform-auth` 校验。 3. 把已校验 claims 写入请求上下文。 4. 在本阶段提供最小内部验收入口 `/_internal/auth/claims`。 不负责: 1. 自己再实现一套 JWT 编解码逻辑。 2. 把 claims 结构拆散成多个重复 helper。 ## 5. 配置口径 当前阶段 `api-server` 读取并传入 `platform-auth` 的配置如下: | 配置项 | 环境变量 | 默认值 | 说明 | | --- | --- | --- | --- | | issuer | `GENARRATIVE_JWT_ISSUER` | `https://auth.genarrative.local` | OIDC 风格发行者标识。 | | secret | `GENARRATIVE_JWT_SECRET` | `genarrative-dev-secret` | 当前阶段沿用对称签名密钥。 | | access token TTL | `GENARRATIVE_JWT_ACCESS_TOKEN_TTL_SECONDS` | `7200` | access token 有效期,单位秒。 | 兼容回退: 1. `issuer` 可回退读取 `JWT_ISSUER`。 2. `secret` 可回退读取 `JWT_SECRET`。 3. TTL 可回退读取 `JWT_EXPIRES_IN`,支持: - 纯秒值,例如 `900` - `s/m/h/d` 后缀,例如 `30m`、`2h` ## 6. 算法选择 当前阶段固定采用: 1. `alg = HS256` 理由: 1. 与当前 Node 基线兼容,迁移阻力最低。 2. 先把 claims、配置口径和 Bearer 主链稳定下来。 3. 若未来升级到非对称签名,应作为独立任务处理,而不是夹带进当前重写链路。 ## 7. Rust 结构设计 ### 7.1 `AccessTokenClaimsInput` 用途: 1. 作为业务层输入。 2. 不承载 `iat/exp/iss` 这种平台计算字段。 字段: 1. `user_id` 2. `session_id` 3. `provider` 4. `roles` 5. `token_version` 6. `phone_verified` 7. `binding_status` 8. `display_name` ### 7.2 `AccessTokenClaims` 用途: 1. 对应最终 JWT payload。 2. 直接用于签名与验签结果输出。 字段: 1. `iss` 2. `sub` 3. `sid` 4. `provider` 5. `roles` 6. `ver` 7. `phone_verified` 8. `binding_status` 9. `display_name` 10. `iat` 11. `exp` ## 8. 校验规则 ### 8.1 签发前校验 1. `issuer`、`secret` 不能为空。 2. TTL 必须大于 `0`。 3. `sub` 不能为空。 4. `sid` 不能为空。 5. `roles` 不能为空数组。 6. `exp` 必须晚于 `iat`。 ### 8.2 验签时校验 1. 算法必须是 `HS256`。 2. 签名必须正确。 3. `iss` 必须匹配当前配置。 4. `exp/iat/iss/sub` 必须存在。 5. 反序列化后的 `sid/provider/roles/ver/phone_verified/binding_status` 必须完整。 当前阶段不做的校验: 1. `ver` 与数据库最新 token version 比对。 2. `sid` 与 `refresh_session` 活跃状态比对。 3. `roles` 的细粒度授权判断。 ## 9. api-server 最小接线 本阶段 `api-server` 接线规则如下: 1. `AppConfig` 增加 JWT 相关配置。 2. `AppState` 在启动时构造唯一一份 `JwtConfig`。 3. `require_bearer_auth` 中间件从请求头读取 Bearer token。 4. 验签成功后把 claims 以 `AuthenticatedAccessToken` 写入 request extensions。 5. 内部路由 `/_internal/auth/claims` 用于返回当前已校验 claims,作为阶段验收与调试入口。 说明: 1. 这个内部路由不是最终对外 contract。 2. 它的存在是为了在 `module-auth` 与正式 `/api/auth/me` 落地前,先把 Bearer 主链单独跑通。 ## 10. 测试策略 当前阶段要求至少覆盖: 1. `platform-auth` 的 JWT 签发与验签回环。 2. issuer 不匹配时拒绝。 3. 空角色拒绝。 4. `api-server` 在无 Bearer token 时返回 `401`。 5. `api-server` 在合法 Bearer token 下返回 claims。 ## 11. 完成定义 当以下条件满足时,本任务视为完成: 1. Rust workspace 中存在真实可编译的 `platform-auth` crate。 2. `api-server` 已能使用 `platform-auth` 校验 Bearer JWT。 3. 工作区测试与编译可通过。 4. 任务清单已同步更新。 ## 12. 后续衔接 下一阶段继续衔接: 1. refresh cookie 读取与轮换。 2. `module-auth` 会话真相与 `token_version` 在线校验。 3. `/api/auth/me`、`/api/auth/refresh` 等正式接口。 4. SpacetimeDB 对 Axum JWT 的身份透传验证。