Files
Genarrative/docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md

15 KiB
Raw Permalink Blame History

refresh_session 表设计

日期:2026-04-21

1. 文档目的

这份文档用于完成 M2 的第三条任务:设计 refresh_session

目标是把以下几件事固定到可编码级别:

  1. refresh cookie 与服务端 session 表的边界
  2. refreshlogoutlogout-allsessions/:sessionId/revoke 的失效语义
  3. refresh_sessionuser_account.token_version 的职责切分
  4. 会话列表、当前设备识别、轮换与吊销的数据结构

补充约束:

  1. 多端登录识别字段以 MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md 为准。
  2. 本文负责把这些字段正式并入 refresh_session 结构、迁移规则与接口读模型。

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 的职责切开。
  4. 旧 Node 基线只有最小 client_type + user_agent + ip 粒度,本轮需要升级为结构化客户端身份模型。

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

浏览器侧固定继续存:

  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/sessionsisCurrent 固定按以下规则判断:

  1. 从 cookie 读出原始 refresh token
  2. 计算 hash
  3. refresh_session.refresh_token_hash 比较
  4. 若 refresh cookie 缺失或不可用,再使用 Bearer access token claims 中的 sidrefresh_session.session_id 比较
  5. 会话列表按“同设备 + 同 IP”聚合时组内任一 session 命中当前 hash 或当前 sid,整组都视为当前设备组

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 终端大类,固定枚举见多端会话身份设计。
client_runtime String 具体运行时,例如 chromewechat_mini_program
client_platform String 平台类型,例如 windowsiosandroid
client_instance_id Option<String> 客户端实例 ID用于区分同设备不同浏览器或同浏览器不同安装实例。
device_fingerprint Option<String> 服务端派生的设备聚类指纹,不作为安全凭据。
device_display_name String 用于会话列表展示的统一端侧名称。
mini_program_app_id Option<String> 小程序 appid。
mini_program_env Option<String> 小程序环境,例如 developtrialrelease
user_agent Option<String> 请求头中的 User-Agent 原文。
ip Option<String> 会话创建时采集的客户端 IP。
issued_by_provider String 该会话是由哪种登录链路创建,枚举固定为 passwordphonewechat
expires_at String 当前 refresh token 过期时间UTC RFC3339。
revoked_at Option<String> 会话被吊销的时间。
revoked_reason_code Option<String> 吊销原因码,例如 logoutlogout_allsession_revokeaccount_disabled
created_at String 会话首次创建时间。
updated_at String 最近一次会话状态变更时间。
last_seen_at String 最近一次 refresh 成功或创建时的活跃时间。

补充说明:

  1. 当前阶段时间字段统一继续使用 UTC RFC3339 字符串。
  2. session_id 在 refresh 轮换时保持不变,不创建新会话行。
  3. issued_by_provider 不是为了做 provider 身份表,而是为了后续账号安全页和审计展示保留稳定字段。
  4. device_display_name 由 Axum 应用层基于结构化字段派生,不直接信任前端自由文本。
  5. client_typeclient_runtimeclient_platform 的具体枚举口径固定受多端会话身份设计文档约束。

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. 如果 refresh cookie 缺失,则回退用 Bearer access token claims 中的 sid 找当前 session
  3. revoked_at = now
  4. revoked_reason_code = logout
  5. 同时提升 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
  6. 撤销后必须同步 auth store 到 SpacetimeDB

读取约束:

  1. Bearer JWT 中的 sid 必须对应 active refresh_session
  2. 被该接口撤销的设备即使 access token 未过期,后续请求也必须立刻返回未授权
  3. 该接口不承担当前设备退出语义;当前设备退出固定走 /api/auth/logout

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. client_runtime
  4. client_platform
  5. device_display_name
  6. mini_program_app_id
  7. mini_program_env
  8. user_agent
  9. ip
  10. created_at
  11. last_seen_at
  12. expires_at

前端 DTO 侧:

  1. clientLabel 当前阶段继续兼容保留,但固定与 deviceDisplayName 对齐。
  2. ipMaskedisCurrent 继续在 Axum 侧派生。
  3. 同设备同 IP 的 active sessions 由 Axum 聚合后返回一条记录。
  4. sessionId 是代表 ID当前组代表 ID 使用当前 sid 对应 session。
  5. sessionIds 返回组内全部 active session IDsessionCount 返回组内数量。
  6. 聚合组时间语义:createdAt 取最早创建时间,lastSeenAtexpiresAt 取最新值。

10.3 POST /api/auth/logout

依赖:

  1. 当前 cookie 命中的 refresh_session
  2. cookie 缺失时 Bearer sid 命中的 refresh_session
  3. user_account.token_version

10.4 POST /api/auth/logout-all

依赖:

  1. 当前 user_id 下全部活跃 refresh_session
  2. user_account.token_version

10.5 POST /api/auth/sessions/{sessionId}/revoke

依赖:

  1. 当前 Bearer JWT 的 user_id
  2. 当前 Bearer JWT 的 sid
  3. 目标 refresh_session.session_id
  4. refresh_session.revoked_at
  5. refresh_session.expires_at

固定行为:

  1. 目标 session 必须属于当前用户
  2. 目标 session 不能是当前 sid
  3. 成功只撤销目标 session不递增 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 browser 在迁移后统一归一为 web_browser
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
  3. client_runtime 初次迁移按 User-Agent 粗判,无法识别时回填 unknown
  4. client_platform 初次迁移按 User-Agent 粗判,无法识别时回填 unknown
  5. client_instance_id 初次迁移统一回填为 null
  6. device_fingerprint 初次迁移按 client_type + client_runtime + client_platform + normalized_user_agent 派生
  7. device_display_name 初次迁移由后端按多端会话身份规则派生
  8. mini_program_app_id 初次迁移统一回填为 null
  9. mini_program_env 初次迁移统一回填为 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_sessionuser_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