12 KiB
12 KiB
wechat_auth_state 表设计
日期:2026-04-21
1. 文档目的
这份文档用于完成 M2 的第七条任务:设计 wechat_auth_state。
目标是把当前只存在于 Node 进程内存中的微信 OAuth state 临时仓,升级为一张可跨实例、可过期、可单次消费的 SpacetimeDB private table,并固定:
- 微信登录
state的职责边界 wechat/start与wechat/callback的写入/消费顺序- 活跃态、已消费态、已过期态的判定规则
- 它与
auth_identity、refresh_session、auth_audit_log的边界
2. 当前基线
当前 Node 后端并没有数据库表,而是使用进程内临时 Map 维护微信登录状态:
server-node/src/services/wechatAuthStateStore.tsserver-node/src/auth/authService.tsserver-node/src/routes/authRoutes.ts
当前 Node WechatAuthStateStore 字段基线只有三项:
stateredirectPathcreatedAt
当前 Node 已落地行为基线:
GET /api/auth/wechat/start创建一个随机state,并把redirectPath放进内存仓WechatAuthService.buildAuthorizationUrl(...)使用该state生成微信授权 URLGET /api/auth/wechat/callback进入时先consume(state),命中则立刻从内存仓删除- 若
state未命中,则回退到默认redirectPath并带auth_error - 即使后续微信
code兑换、账号创建或绑定失败,当前state也不会恢复成可再次使用
当前实现的主要问题:
- 状态仅存在于单进程内存,无法支撑 Axum 多实例部署
- 进程重启后所有未完成的微信登录都会失效
- 当前没有显式过期时间与清理策略
- 当前
startWechatLogin(...)会先创建state,再校验授权场景;若是“普通手机浏览器非微信内打开”,会产生无法使用的脏状态
3. 表职责边界
3.1 wechat_auth_state 负责
- 保存一次微信登录发起时生成的随机
state - 保存与这次
state绑定的redirect_path - 保存本次授权场景
scene - 保存
state的过期时间与消费时间 - 作为
wechat/callback单次消费判定的唯一事实来源
3.2 它不负责
- 保存微信
code、access_token、refresh_token - 保存微信用户资料或 provider 身份绑定结果
- 保存长期登录会话
- 承担账号安全审计展示
3.3 与其他表的边界
wechat_auth_state只负责“这次 OAuth 跳转是否合法、是否还能被消费”auth_identity负责“微信 provider 身份最终绑定到哪个账号”refresh_session负责“微信登录成功后生成的长期浏览器登录态”auth_audit_log负责“微信登录成功、绑定手机号等长期可追溯安全事件”
4. 访问级别
wechat_auth_state 固定为 private table。
原因:
state本身就是一次性安全令牌,不应该暴露给前端以外的查询面redirect_path、request_user_agent都属于登录上下文数据- 这张表只服务于 Axum 鉴权应用层,不应被前端直接查询或订阅
5. 字段设计
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
wechat_state_id |
String |
是 | 主键,建议沿用 wxstate_* 前缀。 |
state_token |
String |
是 | 发往微信 OAuth 的随机 state 原文,固定唯一。 |
redirect_path |
String |
是 | 已归一化后的相对跳转路径,始终为站内路径。 |
scene |
String |
是 | 授权场景,枚举固定为 desktop、wechat_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,消费时更新。 |
补充约束:
state_token继续兼容当前 Node 语义,默认由18字节随机数生成36位十六进制字符串。redirect_path必须先经过当前normalizeRedirectPath(...)规则归一化后再落表,不保存原始外部 URL。request_user_agent仅用于短期问题排查与场景回放,建议在 Axum 侧截断到1024字节以内。- 当前阶段不保存
request_ip,避免把短期 OAuth 状态表扩成额外的风控上下文表;若后续需要 IP 维度审计,应由auth_audit_log或独立风控表承担。
6. 场景与状态语义设计
6.1 scene
当前阶段固定只支持:
desktopwechat_in_app
解释:
desktop对应桌面浏览器发起的微信登录wechat_in_app对应微信内浏览器发起的微信登录
补充约束:
- 普通手机浏览器且非微信内打开,不进入
wechat_auth_state创建流程,直接按当前产品规则返回错误。 scene必须在wechat/start创建表记录前先解析完成,避免写入无效状态。
6.2 活跃态
一条 wechat_auth_state 同时满足以下条件,才视为活跃可用:
consumed_at = nullexpires_at > now
6.3 已消费态
满足以下条件时,视为已消费:
consumed_at != null
说明:
- 已消费态不区分后续微信回调业务成功还是失败。
- 只要进入 callback 并通过单次消费校验,这条
state就不能再次复用。
6.4 已过期态
满足以下条件时,视为已过期:
consumed_at = nullexpires_at <= now
说明:
- 过期态不要求立即更新行状态。
- 读取活跃态时自然排除。
7. 时效与清理策略
7.1 state 有效期
当前阶段固定设计为短时有效态:
- 默认 TTL:
15分钟 - 实际值由 Axum 配置提供,建议新增
wechat_auth.state_ttl_minutes
设计原因:
- 需要覆盖桌面端扫码与微信内授权的正常完成窗口
- 不能无限期保留可回放的 OAuth
state
7.2 清理保留期
当前阶段建议保留短期排障窗口后再清理:
- 已消费记录:
consumed_at之后保留24小时 - 已过期未消费记录:
expires_at之后保留24小时
说明:
- 不在消费成功时立即删行,避免短期内无法排查重复回调、授权失败等问题
- 这张表不是审计表,不允许无限期堆积
8. 写入与消费规则
8.1 GET /api/auth/wechat/start
固定流程:
- 先归一化
redirectPath;空值或非法值回退到默认redirectPath - 先根据
userAgent解析scene - 若场景是“普通手机浏览器且非微信内打开”,直接返回错误,不写表
- 生成随机
state_token - 计算
expires_at = now + ttl - 写入一条新的
wechat_auth_state - 使用
state_token + scene + callbackUrl生成最终授权 URL
关键约束:
- 每次点击微信登录都创建新记录,不复用旧记录。
- 不按
redirect_path去重,允许同一用户在多个标签页并行发起微信登录。 - 只有场景校验通过后才允许写表,避免制造无意义脏数据。
8.2 GET /api/auth/wechat/callback
固定流程:
- 读取请求里的
state - 若
state为空,直接使用默认redirectPath重定向并带auth_error - 按
state_token查询对应记录 - 若记录不存在、已过期或已消费,直接使用默认
redirectPath重定向并带auth_error - 若命中活跃记录,先缓存其
redirect_path - 在进行微信
code兑换、身份查找、账号创建前,先执行单次消费:consumed_at = nowupdated_at = now
- 若单次消费成功,再继续后续微信登录主链
- 后续主链无论成功还是失败,都使用第
5步缓存的redirect_path进行最终跳转
关键约束:
state必须“先消费、后换取微信用户资料”,保持和当前 Node 行为一致,避免同一state被重复回放。- 消费成功后,即使后续 provider 失败、用户创建失败或绑定失败,也不回滚
consumed_at。 - 竞争消费时只允许首个请求成功,后到请求一律视为无效或已失效回调。
9. 唯一约束与索引
9.1 必须具备的唯一约束
wechat_state_id主键唯一state_token全局唯一
9.2 必须具备的查询索引
(state_token)作用:支撑 callback 按state精确查找(expires_at)作用:支撑过期数据清理(consumed_at)作用:支撑已消费数据清理
说明:
- 当前阶段不需要按
redirect_path、scene建业务查询索引,因为主链只按state_token精确查找。 - 清理作业以时间窗口为主,不需要复杂多列排序索引。
10. 读取规则
当前阶段 wechat_auth_state 不直接对外暴露 DTO。
它只支撑后端内部这几类读取:
find_by_state_token(state_token)find_active_by_state_token(state_token)list_expired_before(deadline)list_consumed_before(deadline)
读取约束:
- “是否活跃”由应用层按
consumed_at与expires_at判定,不引入额外状态枚举列。 - 读取命中后,
redirect_path只用于当前 callback 的最终跳转,不向前端原样暴露为查询接口。 - 前端不允许直接查看自己仍持有多少个待消费微信
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 层
必须至少具备:
create_wechat_auth_stateconsume_wechat_auth_statepurge_wechat_auth_state
说明:
create_wechat_auth_state只负责插入新的待消费记录。consume_wechat_auth_state只允许消费“当前仍活跃”的记录。purge_wechat_auth_state只负责清理保留期外的已消费或已过期记录。
12.2 Axum 应用层
固定负责:
- 归一化
redirectPath - 根据
userAgent判定scene - 生成随机
state_token - 计算
expires_at - 在 callback 中先读取、再执行单次消费、再继续微信 provider 主链
- 对“无效 state / 过期 state / 已消费 state”统一生成兼容当前前端的
auth_error跳转结果
13. 不允许的设计漂移
后续实现时禁止出现以下情况:
- 继续把微信 OAuth
state只放在 Axum 进程内存里,当成多实例时代的真相源 - 在普通手机浏览器非微信内打开时仍然先创建
wechat_auth_state - 为了省步骤,把
state改成“登录成功后再消费” - 把微信
code、access_token、用户资料 JSON 直接写进wechat_auth_state - 消费成功后立刻硬删除记录,导致无法区分“重复回调”与“从未发起过该 state”
- 把
wechat_auth_state暴露成前端可直接查询的公共表或订阅面
14. 本任务完成定义
当以下条件满足时,设计 wechat_auth_state 视为完成:
- 当前 Node 内存状态仓已被完整映射为可落表的短期状态模型。
wechat/start与wechat/callback的写入、消费、过期、清理规则已固定。- 已和
auth_identity、refresh_session、auth_audit_log明确切开职责。 - 后续可以直接按这份文档编码
module-authreducer、Axum 仓储接口与清理任务。
15. 依据文件
server-node/src/services/wechatAuthStateStore.tsserver-node/src/auth/authService.tsserver-node/src/routes/authRoutes.tsserver-node/src/services/wechatAuthService.tspackages/shared/src/contracts/auth.tsdocs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.mddocs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.mddocs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md