Files
Genarrative/docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

12 KiB
Raw Blame History

wechat_auth_state 表设计

日期:2026-04-21

1. 文档目的

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

目标是把当前只存在于 Node 进程内存中的微信 OAuth state 临时仓,升级为一张可跨实例、可过期、可单次消费的 SpacetimeDB private table,并固定:

  1. 微信登录 state 的职责边界
  2. wechat/startwechat/callback 的写入/消费顺序
  3. 活跃态、已消费态、已过期态的判定规则
  4. 它与 auth_identityrefresh_sessionauth_audit_log 的边界

2. 当前基线

当前 Node 后端并没有数据库表,而是使用进程内临时 Map 维护微信登录状态:

  1. server-node/src/services/wechatAuthStateStore.ts
  2. server-node/src/auth/authService.ts
  3. server-node/src/routes/authRoutes.ts

当前 Node WechatAuthStateStore 字段基线只有三项:

  1. state
  2. redirectPath
  3. createdAt

当前 Node 已落地行为基线:

  1. GET /api/auth/wechat/start 创建一个随机 state,并把 redirectPath 放进内存仓
  2. WechatAuthService.buildAuthorizationUrl(...) 使用该 state 生成微信授权 URL
  3. GET /api/auth/wechat/callback 进入时先 consume(state),命中则立刻从内存仓删除
  4. state 未命中,则回退到默认 redirectPath 并带 auth_error
  5. 即使后续微信 code 兑换、账号创建或绑定失败,当前 state 也不会恢复成可再次使用

当前实现的主要问题:

  1. 状态仅存在于单进程内存,无法支撑 Axum 多实例部署
  2. 进程重启后所有未完成的微信登录都会失效
  3. 当前没有显式过期时间与清理策略
  4. 当前 startWechatLogin(...) 会先创建 state,再校验授权场景;若是“普通手机浏览器非微信内打开”,会产生无法使用的脏状态

3. 表职责边界

3.1 wechat_auth_state 负责

  1. 保存一次微信登录发起时生成的随机 state
  2. 保存与这次 state 绑定的 redirect_path
  3. 保存本次授权场景 scene
  4. 保存 state 的过期时间与消费时间
  5. 作为 wechat/callback 单次消费判定的唯一事实来源

3.2 它不负责

  1. 保存微信 codeaccess_tokenrefresh_token
  2. 保存微信用户资料或 provider 身份绑定结果
  3. 保存长期登录会话
  4. 承担账号安全审计展示

3.3 与其他表的边界

  1. wechat_auth_state 只负责“这次 OAuth 跳转是否合法、是否还能被消费”
  2. auth_identity 负责“微信 provider 身份最终绑定到哪个账号”
  3. refresh_session 负责“微信登录成功后生成的长期浏览器登录态”
  4. auth_audit_log 负责“微信登录成功、绑定手机号等长期可追溯安全事件”

4. 访问级别

wechat_auth_state 固定为 private table

原因:

  1. state 本身就是一次性安全令牌,不应该暴露给前端以外的查询面
  2. redirect_pathrequest_user_agent 都属于登录上下文数据
  3. 这张表只服务于 Axum 鉴权应用层,不应被前端直接查询或订阅

5. 字段设计

字段名 类型 必填 说明
wechat_state_id String 主键,建议沿用 wxstate_* 前缀。
state_token String 发往微信 OAuth 的随机 state 原文,固定唯一。
redirect_path String 已归一化后的相对跳转路径,始终为站内路径。
scene String 授权场景,枚举固定为 desktopwechat_in_app
request_user_agent Option<String> 发起授权时的 UA 原文快照;缺失时为 null
expires_at String 当前 state 的失效时间UTC RFC3339。
consumed_at Option<String> 首次被成功消费的时间;未消费时为 null
created_at String 创建时间。
updated_at String 最近一次状态变更时间;创建时等于 created_at,消费时更新。

补充约束:

  1. state_token 继续兼容当前 Node 语义,默认由 18 字节随机数生成 36 位十六进制字符串。
  2. redirect_path 必须先经过当前 normalizeRedirectPath(...) 规则归一化后再落表,不保存原始外部 URL。
  3. request_user_agent 仅用于短期问题排查与场景回放,建议在 Axum 侧截断到 1024 字节以内。
  4. 当前阶段不保存 request_ip,避免把短期 OAuth 状态表扩成额外的风控上下文表;若后续需要 IP 维度审计,应由 auth_audit_log 或独立风控表承担。

6. 场景与状态语义设计

6.1 scene

当前阶段固定只支持:

  1. desktop
  2. wechat_in_app

解释:

  1. desktop 对应桌面浏览器发起的微信登录
  2. wechat_in_app 对应微信内浏览器发起的微信登录

补充约束:

  1. 普通手机浏览器且非微信内打开,不进入 wechat_auth_state 创建流程,直接按当前产品规则返回错误。
  2. scene 必须在 wechat/start 创建表记录前先解析完成,避免写入无效状态。

6.2 活跃态

一条 wechat_auth_state 同时满足以下条件,才视为活跃可用:

  1. consumed_at = null
  2. expires_at > now

6.3 已消费态

满足以下条件时,视为已消费:

  1. consumed_at != null

说明:

  1. 已消费态不区分后续微信回调业务成功还是失败。
  2. 只要进入 callback 并通过单次消费校验,这条 state 就不能再次复用。

6.4 已过期态

满足以下条件时,视为已过期:

  1. consumed_at = null
  2. expires_at <= now

说明:

  1. 过期态不要求立即更新行状态。
  2. 读取活跃态时自然排除。

7. 时效与清理策略

7.1 state 有效期

当前阶段固定设计为短时有效态:

  1. 默认 TTL15 分钟
  2. 实际值由 Axum 配置提供,建议新增 wechat_auth.state_ttl_minutes

设计原因:

  1. 需要覆盖桌面端扫码与微信内授权的正常完成窗口
  2. 不能无限期保留可回放的 OAuth state

7.2 清理保留期

当前阶段建议保留短期排障窗口后再清理:

  1. 已消费记录:consumed_at 之后保留 24 小时
  2. 已过期未消费记录:expires_at 之后保留 24 小时

说明:

  1. 不在消费成功时立即删行,避免短期内无法排查重复回调、授权失败等问题
  2. 这张表不是审计表,不允许无限期堆积

8. 写入与消费规则

8.1 GET /api/auth/wechat/start

固定流程:

  1. 先归一化 redirectPath;空值或非法值回退到默认 redirectPath
  2. 先根据 userAgent 解析 scene
  3. 若场景是“普通手机浏览器且非微信内打开”,直接返回错误,不写表
  4. 生成随机 state_token
  5. 计算 expires_at = now + ttl
  6. 写入一条新的 wechat_auth_state
  7. 使用 state_token + scene + callbackUrl 生成最终授权 URL

关键约束:

  1. 每次点击微信登录都创建新记录,不复用旧记录。
  2. 不按 redirect_path 去重,允许同一用户在多个标签页并行发起微信登录。
  3. 只有场景校验通过后才允许写表,避免制造无意义脏数据。

8.2 GET /api/auth/wechat/callback

固定流程:

  1. 读取请求里的 state
  2. state 为空,直接使用默认 redirectPath 重定向并带 auth_error
  3. state_token 查询对应记录
  4. 若记录不存在、已过期或已消费,直接使用默认 redirectPath 重定向并带 auth_error
  5. 若命中活跃记录,先缓存其 redirect_path
  6. 在进行微信 code 兑换、身份查找、账号创建前,先执行单次消费:
    • consumed_at = now
    • updated_at = now
  7. 若单次消费成功,再继续后续微信登录主链
  8. 后续主链无论成功还是失败,都使用第 5 步缓存的 redirect_path 进行最终跳转

关键约束:

  1. state 必须“先消费、后换取微信用户资料”,保持和当前 Node 行为一致,避免同一 state 被重复回放。
  2. 消费成功后,即使后续 provider 失败、用户创建失败或绑定失败,也不回滚 consumed_at
  3. 竞争消费时只允许首个请求成功,后到请求一律视为无效或已失效回调。

9. 唯一约束与索引

9.1 必须具备的唯一约束

  1. wechat_state_id 主键唯一
  2. state_token 全局唯一

9.2 必须具备的查询索引

  1. (state_token) 作用:支撑 callback 按 state 精确查找
  2. (expires_at) 作用:支撑过期数据清理
  3. (consumed_at) 作用:支撑已消费数据清理

说明:

  1. 当前阶段不需要按 redirect_pathscene 建业务查询索引,因为主链只按 state_token 精确查找。
  2. 清理作业以时间窗口为主,不需要复杂多列排序索引。

10. 读取规则

当前阶段 wechat_auth_state 不直接对外暴露 DTO。

它只支撑后端内部这几类读取:

  1. find_by_state_token(state_token)
  2. find_active_by_state_token(state_token)
  3. list_expired_before(deadline)
  4. list_consumed_before(deadline)

读取约束:

  1. “是否活跃”由应用层按 consumed_atexpires_at 判定,不引入额外状态枚举列。
  2. 读取命中后,redirect_path 只用于当前 callback 的最终跳转,不向前端原样暴露为查询接口。
  3. 前端不允许直接查看自己仍持有多少个待消费微信 state

11. 与当前 Node 内存状态仓的映射关系

Node 字段/行为 新字段/行为 迁移规则
state state_token 原语义保留,继续作为微信 OAuth 回调校验值。
redirectPath redirect_path 重命名迁移;写入前必须先归一化为站内路径。
createdAt created_at 原样迁移为 UTC RFC3339。
wechat_state_id 新增内部主键。
scene 新增授权场景字段,值来自 userAgent 解析。
request_user_agent 新增请求上下文字段,用于短期排障。
expires_at 新增显式过期时间。
consume(state) 后直接删除 consumed_at 标记消费 改为“标记消费 + 延迟清理”,不再立刻硬删除。
updated_at 新增状态更新时间,用于消费与清理追踪。

12. reducer / service 落地约束

12.1 module-auth reducer 层

必须至少具备:

  1. create_wechat_auth_state
  2. consume_wechat_auth_state
  3. purge_wechat_auth_state

说明:

  1. create_wechat_auth_state 只负责插入新的待消费记录。
  2. consume_wechat_auth_state 只允许消费“当前仍活跃”的记录。
  3. purge_wechat_auth_state 只负责清理保留期外的已消费或已过期记录。

12.2 Axum 应用层

固定负责:

  1. 归一化 redirectPath
  2. 根据 userAgent 判定 scene
  3. 生成随机 state_token
  4. 计算 expires_at
  5. 在 callback 中先读取、再执行单次消费、再继续微信 provider 主链
  6. 对“无效 state / 过期 state / 已消费 state”统一生成兼容当前前端的 auth_error 跳转结果

13. 不允许的设计漂移

后续实现时禁止出现以下情况:

  1. 继续把微信 OAuth state 只放在 Axum 进程内存里,当成多实例时代的真相源
  2. 在普通手机浏览器非微信内打开时仍然先创建 wechat_auth_state
  3. 为了省步骤,把 state 改成“登录成功后再消费”
  4. 把微信 codeaccess_token、用户资料 JSON 直接写进 wechat_auth_state
  5. 消费成功后立刻硬删除记录,导致无法区分“重复回调”与“从未发起过该 state”
  6. wechat_auth_state 暴露成前端可直接查询的公共表或订阅面

14. 本任务完成定义

当以下条件满足时,设计 wechat_auth_state 视为完成:

  1. 当前 Node 内存状态仓已被完整映射为可落表的短期状态模型。
  2. wechat/startwechat/callback 的写入、消费、过期、清理规则已固定。
  3. 已和 auth_identityrefresh_sessionauth_audit_log 明确切开职责。
  4. 后续可以直接按这份文档编码 module-auth reducer、Axum 仓储接口与清理任务。

15. 依据文件

  1. server-node/src/services/wechatAuthStateStore.ts
  2. server-node/src/auth/authService.ts
  3. server-node/src/routes/authRoutes.ts
  4. server-node/src/services/wechatAuthService.ts
  5. packages/shared/src/contracts/auth.ts
  6. docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md
  7. docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md
  8. docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md