From 5dd7528280bb8e78e4cbf85376b7f63b152dc134 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Apr 2026 02:10:43 +0800 Subject: [PATCH] docs: design auth refresh session table --- .../01_M0_M2_FOUNDATION_AND_AUTH.md | 3 +- docs/technical/README.md | 1 + ...M_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md | 4 + ...REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md | 388 ++++++++++++++++++ server-rs/packages/module-auth/README.md | 1 + 5 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md index 84e9fba2..524e9695 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -144,7 +144,8 @@ 交付物:[../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md) - [x] 设计 `auth_identity` 交付物:[../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) -- [ ] 设计 `refresh_session` +- [x] 设计 `refresh_session` + 交付物:[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) - [ ] 设计 `auth_audit_log` - [ ] 设计 `auth_risk_block` - [ ] 设计 `sms_auth_event` diff --git a/docs/technical/README.md b/docs/technical/README.md index 05b5b80d..bfcd05ff 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,6 +4,7 @@ ## 文档列表 +- [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md):`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换与吊销语义、索引与迁移规则。 - [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md):`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。 - [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md):`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。 - [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。 diff --git a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md index 0f7d19a9..6f393917 100644 --- a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md +++ b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md @@ -314,6 +314,10 @@ server-rs/ - [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) +`refresh_session` 的 cookie/hash 边界、轮换与吊销语义,见: + +- [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) + ### B. 运行时主状态表 - `runtime_snapshot` diff --git a/docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md b/docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md new file mode 100644 index 00000000..93eff9af --- /dev/null +++ b/docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md @@ -0,0 +1,388 @@ +# `refresh_session` 表设计 + +日期:`2026-04-21` + +## 1. 文档目的 + +这份文档用于完成 `M2` 的第三条任务:`设计 refresh_session`。 + +目标是把以下几件事固定到可编码级别: + +1. refresh cookie 与服务端 session 表的边界 +2. `refresh`、`logout`、`logout-all`、`sessions/:sessionId/revoke` 的失效语义 +3. `refresh_session` 与 `user_account.token_version` 的职责切分 +4. 会话列表、当前设备识别、轮换与吊销的数据结构 + +## 2. 当前基线 + +当前 Node 后端已经存在一张 `user_sessions` 表,并且 refresh cookie 主链已经完整可用: + +1. 登录成功后创建随机 refresh token,并只把原始 token 放入 HttpOnly cookie +2. 服务端只存 `sha256(refresh_token)` 结果 +3. `/api/auth/refresh` 会轮换 refresh token,同时更新过期时间与 `last_seen_at` +4. `/api/auth/logout` 会吊销当前 refresh session,并提升 `token_version` +5. `/api/auth/logout-all` 会吊销当前账号全部 refresh session,并提升 `token_version` +6. `/api/auth/sessions` 依赖会话表列出当前设备与远端设备 +7. `/api/auth/sessions/:sessionId/revoke` 只吊销目标设备,不影响当前设备 + +当前 Node `user_sessions` 字段基线: + +1. `id` +2. `user_id` +3. `refresh_token_hash` +4. `client_type` +5. `user_agent` +6. `ip` +7. `expires_at` +8. `revoked_at` +9. `created_at` +10. `updated_at` +11. `last_seen_at` + +这说明: + +1. refresh session 已经是现有系统的既有真相源。 +2. Rust 重写时不需要重新发明另一套“session cache + cookie state”双轨模型。 +3. 只需要把当前语义更明确地迁入 SpacetimeDB,并把与 `user_account` 的职责切开。 + +## 3. 边界定义 + +### 3.1 `refresh_session` 负责 + +1. 设备级 refresh token hash 真相 +2. 设备级过期时间 +3. 设备级吊销状态 +4. 设备级最后活跃时间 +5. 会话列表所需的客户端信息 + +### 3.2 它不负责 + +1. access token 签发 +2. access token 全局失效版本号 +3. 用户主状态 +4. provider 身份绑定 +5. 短信验证码与微信 OAuth + +### 3.3 与 `user_account` 的职责切分 + +固定规则: + +1. `refresh_session` 负责“哪台设备还能继续 refresh” +2. `user_account.token_version` 负责“旧 access token 是否整体失效” + +因此: + +1. `logout` 必须同时改两层 +2. `logout-all` 必须同时改两层 +3. `sessions/:sessionId/revoke` 只改 `refresh_session` +4. `/refresh` 只改 `refresh_session`,不改 `token_version` + +## 4. cookie 与表的边界 + +### 4.1 cookie 只存原始 token + +浏览器侧固定继续存: + +1. cookie 名:`genarrative_refresh_session` +2. 值:原始 refresh token +3. `HttpOnly` +4. `Path=/api/auth` +5. 默认 `SameSite=Lax` +6. 生产环境按配置决定 `Secure` + +### 4.2 表里只存 hash + +`refresh_session` 固定只存: + +1. `sha256(refresh_token)` + +禁止: + +1. 把原始 refresh token 落库 +2. 把原始 refresh token 写日志 +3. 把 cookie 配置字段冗余进表结构 + +### 4.3 当前设备识别方式 + +`/api/auth/sessions` 的 `isCurrent` 固定按以下规则判断: + +1. 从 cookie 读出原始 refresh token +2. 计算 hash +3. 与 `refresh_session.refresh_token_hash` 比较 + +## 5. 表访问级别 + +`refresh_session` 固定为 `private table`。 + +原因: + +1. 包含 refresh token hash +2. 包含客户端 UA 与 IP +3. 包含设备级会话状态 + +前端不直接查询该表,只能通过 Axum / view 聚合后的 DTO 读取。 + +## 6. 字段设计 + +| 字段名 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `session_id` | `String` | 是 | 主键,建议继续沿用 `usess_*` 前缀。 | +| `user_id` | `String` | 是 | 归属账号 ID,外键指向 `user_account.user_id`。 | +| `refresh_token_hash` | `String` | 是 | 当前生效 refresh token 的哈希值。 | +| `client_type` | `String` | 是 | 当前设备类型,当前阶段默认 `browser`。 | +| `user_agent` | `Option` | 否 | 请求头中的 `User-Agent` 原文。 | +| `ip` | `Option` | 否 | 会话创建时采集的客户端 IP。 | +| `issued_by_provider` | `String` | 是 | 该会话是由哪种登录链路创建,枚举固定为 `password`、`phone`、`wechat`。 | +| `expires_at` | `String` | 是 | 当前 refresh token 过期时间,UTC RFC3339。 | +| `revoked_at` | `Option` | 否 | 会话被吊销的时间。 | +| `revoked_reason_code` | `Option` | 否 | 吊销原因码,例如 `logout`、`logout_all`、`session_revoke`、`account_disabled`。 | +| `created_at` | `String` | 是 | 会话首次创建时间。 | +| `updated_at` | `String` | 是 | 最近一次会话状态变更时间。 | +| `last_seen_at` | `String` | 是 | 最近一次 refresh 成功或创建时的活跃时间。 | + +补充说明: + +1. 当前阶段时间字段统一继续使用 UTC RFC3339 字符串。 +2. `session_id` 在 refresh 轮换时保持不变,不创建新会话行。 +3. `issued_by_provider` 不是为了做 provider 身份表,而是为了后续账号安全页和审计展示保留稳定字段。 + +## 7. 唯一约束与索引 + +### 7.1 必须具备的唯一约束 + +1. `session_id` 主键唯一 +2. `refresh_token_hash` 全局唯一 + +### 7.2 必须具备的查询索引 + +1. `(user_id, revoked_at, expires_at, last_seen_at DESC)` + 作用:列当前账号活跃会话 +2. `(user_id, session_id)` + 作用:按用户吊销指定会话 +3. `(expires_at, revoked_at)` + 作用:后续清理过期/已吊销会话 +4. `refresh_token_hash` + 作用:refresh、logout、current session 判断 + +## 8. 生命周期设计 + +### 8.1 创建 + +触发点: + +1. 密码登录成功 +2. 手机号登录成功 +3. 微信登录成功 +4. 微信绑定手机号成功后签发正式会话 + +写入规则: + +1. 生成原始 refresh token +2. 计算 `refresh_token_hash` +3. 创建一条新 `refresh_session` +4. `last_seen_at = created_at` + +### 8.2 刷新 + +触发点: + +1. `POST /api/auth/refresh` + +写入规则: + +1. 先按 `refresh_token_hash` 找当前 session +2. 校验 `revoked_at == null` +3. 校验 `expires_at > now` +4. 生成新的 refresh token +5. 更新同一条 session 的 `refresh_token_hash` +6. 更新 `expires_at` +7. 更新 `last_seen_at` +8. 更新 `updated_at` + +关键约束: + +1. refresh 是“同一会话轮换”,不是“新建第二条会话”。 +2. `session_id` 在轮换前后必须稳定,保证会话列表中的设备 ID 不跳变。 + +### 8.3 吊销当前会话 + +触发点: + +1. `POST /api/auth/logout` + +写入规则: + +1. 按当前 cookie 找 session +2. 写 `revoked_at = now` +3. 写 `revoked_reason_code = logout` +4. 同时提升 `user_account.token_version` + +### 8.4 吊销全部会话 + +触发点: + +1. `POST /api/auth/logout-all` + +写入规则: + +1. 按 `user_id` 批量吊销全部未吊销 session +2. `revoked_reason_code = logout_all` +3. 同时提升 `user_account.token_version` + +### 8.5 吊销指定远端设备 + +触发点: + +1. `POST /api/auth/sessions/:sessionId/revoke` + +写入规则: + +1. 只允许吊销同一 `user_id` 下的目标 session +2. 当前设备不允许通过该接口吊销自己 +3. 只改目标 `refresh_session` +4. `revoked_reason_code = session_revoke` +5. 不提升 `token_version` + +### 8.6 账号被禁用或并入 + +触发点: + +1. `user_account.account_status = disabled` + +写入规则: + +1. 该账号下所有未吊销 session 都必须被批量吊销 +2. `revoked_reason_code = account_disabled` + +## 9. 活跃态判断规则 + +一条 `refresh_session` 只有同时满足以下条件,才视为活跃: + +1. `revoked_at = null` +2. `expires_at > now` +3. 所属 `user_account.account_status = active` 或允许 refresh 的待绑定状态 + +补充约束: + +1. 当前阶段 `pending_bind_phone` 的微信壳账号允许 refresh,但只允许继续走绑定手机号相关接口。 +2. `disabled` 账号无论 session 本身是否过期,都不能继续 refresh。 + +## 10. 与现有接口的映射 + +### 10.1 `POST /api/auth/refresh` + +依赖: + +1. `refresh_session.refresh_token_hash` +2. `refresh_session.expires_at` +3. `refresh_session.revoked_at` +4. `user_account.account_status` +5. `user_account.token_version` + +### 10.2 `GET /api/auth/sessions` + +直接读取: + +1. `session_id` +2. `client_type` +3. `user_agent` +4. `ip` +5. `created_at` +6. `last_seen_at` +7. `expires_at` + +前端 DTO `clientLabel`、`ipMasked`、`isCurrent` 继续在 Axum 侧派生。 + +### 10.3 `POST /api/auth/logout` + +依赖: + +1. 当前 cookie 命中的 `refresh_session` +2. `user_account.token_version` + +### 10.4 `POST /api/auth/logout-all` + +依赖: + +1. 当前 `user_id` 下全部活跃 `refresh_session` +2. `user_account.token_version` + +## 11. 与当前 Node `user_sessions` 的映射关系 + +| Node `user_sessions` 列 | 新 `refresh_session` 字段 | 迁移规则 | +| --- | --- | --- | +| `id` | `session_id` | 原样迁移。 | +| `user_id` | `user_id` | 原样迁移。 | +| `refresh_token_hash` | `refresh_token_hash` | 原样迁移。 | +| `client_type` | `client_type` | 原样迁移。 | +| `user_agent` | `user_agent` | 原样迁移。 | +| `ip` | `ip` | 原样迁移。 | +| `expires_at` | `expires_at` | 原样迁移。 | +| `revoked_at` | `revoked_at` | 原样迁移。 | +| `created_at` | `created_at` | 原样迁移。 | +| `updated_at` | `updated_at` | 原样迁移。 | +| `last_seen_at` | `last_seen_at` | 原样迁移。 | + +新增字段回填规则: + +1. `issued_by_provider` + 初次迁移统一回填为 `password` + 说明:这是保守回填值,后续只影响展示,不影响鉴权正确性 +2. `revoked_reason_code` + 初次迁移统一回填为 `null` + +## 12. reducer / service 落地约束 + +### 12.1 `module-auth` reducer 层 + +必须至少具备这些命令入口: + +1. `create_refresh_session` +2. `rotate_refresh_session` +3. `revoke_refresh_session` +4. `revoke_refresh_sessions_by_user` +5. `revoke_refresh_session_by_user_and_session` +6. `touch_refresh_session_last_seen` + +### 12.2 Axum 应用层 + +固定负责: + +1. 生成原始 refresh token +2. 计算 hash +3. 读写 HttpOnly cookie +4. 决定当前调用是创建、轮换还是吊销 +5. 把 `revoked_reason_code` 映射到对应业务语义 + +## 13. 不允许的设计漂移 + +后续实现时禁止出现以下情况: + +1. refresh 轮换时新建第二条 session,而不是更新原 session +2. `sessions/:sessionId/revoke` 顺手提升 `token_version`,导致当前 access token 一起失效 +3. `logout-all` 只提升 `token_version`,却不吊销 refresh session +4. 原始 refresh token 直接入库 +5. 会话表开始承担 `user_account` 状态职责 + +## 14. 本任务完成定义 + +当以下条件满足时,`设计 refresh_session` 视为完成: + +1. refresh cookie 与服务端 session hash 的边界已经明确。 +2. 轮换、当前设备吊销、全部设备吊销三种语义已经切开。 +3. `refresh_session` 与 `user_account.token_version` 的职责已明确。 +4. 字段、唯一约束、索引与迁移规则已具体到可直接编码。 + +## 15. 依据文件 + +1. `server-node/src/routes/authRoutes.ts` +2. `server-node/src/auth/authService.ts` +3. `server-node/src/auth/refreshSessionCookie.ts` +4. `server-node/src/repositories/userSessionRepository.ts` +5. `server-node/src/config.ts` +6. `server-node/src/db/migrations.ts` +7. `server-node/src/app.test.ts` +8. `docs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.md` +9. `docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md` +10. `docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md` +11. `docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md` diff --git a/server-rs/packages/module-auth/README.md b/server-rs/packages/module-auth/README.md index eb8bb31d..e387af68 100644 --- a/server-rs/packages/module-auth/README.md +++ b/server-rs/packages/module-auth/README.md @@ -26,6 +26,7 @@ 1. [../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md) 2. [../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) +3. [../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) ## 3. 边界约束