# `/api/auth/refresh` 轮换落地设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于指导 `M2` 中 `实现 refresh token 轮换` 任务的首版落地,冻结: 1. `POST /api/auth/refresh` 的请求与响应 contract。 2. refresh cookie、服务端 refresh session 与 access token 三者的职责边界。 3. “会话 ID 稳定、refresh token 可轮换”的固定语义。 4. Rust 首版在未切入 SpacetimeDB 前的临时进程内实现方式。 ## 2. 当前基线 当前 Node `/api/auth/refresh` 已具备以下稳定语义: 1. 从 HttpOnly cookie 中读取原始 refresh token。 2. 服务端只按 `refresh_token_hash` 查找当前活跃会话。 3. refresh 成功后,不新建第二条会话,而是在原会话上轮换 refresh token。 4. 轮换时会更新 `expires_at` 与 `last_seen_at`。 5. 成功后返回新的 access token,并写回新的 refresh cookie。 6. 失败时会主动清空 refresh cookie,要求前端重新登录。 Rust 首版必须保留以上语义。 ## 3. 当前阶段范围 本阶段只落以下内容: 1. `module-auth` 增加进程内 refresh session 真相与轮换服务。 2. `api-server` 暴露 `POST /api/auth/refresh`。 3. 登录成功时创建 refresh session。 4. refresh 成功时在原 session 上轮换 refresh token。 5. access token 的 `sid` 固定改为稳定 `session_id`,不再直接复用 refresh token。 本阶段不包含: 1. `/api/auth/logout` 2. `/api/auth/logout-all` 3. `/api/auth/sessions` 4. `/api/auth/sessions/:sessionId/revoke` 5. SpacetimeDB reducer 真正写表 ## 4. contract ### 4.1 请求 1. 方法:`POST` 2. 路径:`/api/auth/refresh` 3. 请求体:空 4. 鉴权来源:refresh cookie ### 4.2 成功响应 ```json { "token": "" } ``` 同时响应头必须写回新的 refresh cookie。 ### 4.3 失败响应 当 refresh token 缺失、会话不存在、会话已过期或用户不存在时: 1. 返回 `401 UNAUTHORIZED` 2. 同时清理 refresh cookie ## 5. 固定语义 ### 5.1 session_id 与 refresh token 必须拆开 从本任务开始固定以下规则: 1. `session_id` 是稳定会话主键。 2. refresh token 是可轮换的会话凭证。 3. access token 的 `sid` 必须写入 `session_id`。 4. refresh 轮换只更新 refresh token,不更改 `session_id`。 禁止继续把 refresh token 直接塞进 JWT `sid`。 ### 5.2 refresh 是“原会话轮换” refresh 成功后: 1. 保留原 `session_id` 2. 生成新的原始 refresh token 3. 用新的 `refresh_token_hash` 覆盖旧值 4. 更新 `expires_at` 5. 更新 `last_seen_at` 不允许新建第二条 session。 ## 6. crate 边界 ### 6.1 `module-auth` 负责: 1. 管理 refresh session 进程内真相。 2. 提供创建 refresh session 与轮换 refresh session 的用例。 3. 提供按 `user_id` 查询用户快照的能力,供 refresh 成功后重新签发 access token。 不负责: 1. 生成原始 refresh token。 2. 读写 cookie。 3. 签发 JWT。 ### 6.2 `platform-auth` 负责: 1. 生成原始 refresh token。 2. 对 refresh token 做哈希。 3. 构造 refresh cookie 的 `Set-Cookie` 头。 4. 从 cookie header 中读取 refresh token。 ### 6.3 `api-server` 负责: 1. 从请求 cookie 中提取 refresh token。 2. 调用 `module-auth` 执行 refresh session 轮换。 3. 根据用户快照与稳定 `session_id` 重新签发 access token。 4. refresh 失败时清理 cookie。 ## 7. 进程内存储模型 当前阶段 `module-auth` 继续使用进程内内存仓储承接 refresh session,字段至少包括: 1. `session_id` 2. `user_id` 3. `refresh_token_hash` 4. `issued_by_provider` 5. `expires_at` 6. `created_at` 7. `updated_at` 8. `last_seen_at` 9. `revoked_at` 说明: 1. 这只是 SpacetimeDB 正式落地前的阶段性实现。 2. 字段命名与语义继续对齐 [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)。 ## 8. 流程 ### 8.1 登录创建 session 密码登录成功后: 1. `api-server` 生成原始 refresh token。 2. `api-server` 计算 `refresh_token_hash`。 3. `module-auth` 创建一条新 session,并返回稳定 `session_id`。 4. `api-server` 用该 `session_id` 写入 access token 的 `sid`。 5. `api-server` 把原始 refresh token 写回 cookie。 ### 8.2 refresh 轮换 session 当请求 `POST /api/auth/refresh` 时: 1. 从 cookie 中读取原始 refresh token。 2. 计算 `refresh_token_hash`。 3. `module-auth` 查找当前活跃 session。 4. 校验 `expires_at > now` 且 `revoked_at == null`。 5. 读取该 session 对应用户。 6. 生成新的原始 refresh token。 7. 用新 hash 更新同一条 session。 8. 返回新的 access token 与新的 refresh cookie。 ## 9. 错误语义 以下情况统一返回 `401`: 1. 缺少 refresh cookie 2. refresh token 命中不到 session 3. refresh session 已过期 4. refresh session 已吊销 5. session 对应用户不存在 错误文案统一保持中文,并沿用“当前登录态已失效,请重新登录”这类恢复导向语义。 ## 10. 测试策略 至少覆盖: 1. 登录成功后可用 cookie 调用 `/api/auth/refresh` 2. refresh 成功会写回新的 cookie 3. refresh 成功返回新的 access token 4. refresh 后旧 refresh token 立即失效 5. 缺少 cookie 时返回 `401` 6. 无效 refresh token 时返回 `401` 且清理 cookie ## 11. 完成定义 满足以下条件时,本任务视为完成: 1. Rust 侧已提供 `POST /api/auth/refresh`。 2. access token `sid` 已改为稳定 `session_id`。 3. refresh token 轮换成功时不创建新会话。 4. refresh 失败时会清理 cookie。 5. 文档、任务清单与测试已同步更新。