From 9404c777038fc8e89de8d14d0cb14617e63e0b1c Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Apr 2026 11:14:31 +0800 Subject: [PATCH] docs: design wechat auth state table --- .../01_M0_M2_FOUNDATION_AND_AUTH.md | 3 +- docs/technical/README.md | 1 + ...M_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md | 4 + ...CHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md | 324 ++++++++++++++++++ server-rs/crates/module-auth/README.md | 3 +- 5 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_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 0f747a4e..dde483f8 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -152,7 +152,8 @@ 交付物:[../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) - [x] 设计 `sms_auth_event` 交付物:[../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) -- [ ] 设计 `wechat_auth_state` +- [x] 设计 `wechat_auth_state` + 交付物:[../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md) ### Axum 鉴权服务 diff --git a/docs/technical/README.md b/docs/technical/README.md index 80fafa76..1e769844 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,6 +4,7 @@ ## 文档列表 +- [SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md):`M2` 第七张微信 OAuth 状态表 `wechat_auth_state` 的字段、过期/消费语义、`wechat/start` 与 `wechat/callback` 的单次消费规则,以及多实例下的清理策略。 - [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md):`M2` 第六张短信鉴权统计表 `sms_auth_event` 的事件范围、统计口径、索引与和风控/审计表的协作边界。 - [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md):`M2` 第五张风控状态表 `auth_risk_block` 的作用域、活跃态、刷新/解除规则与读取派生约束。 - [SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md):`M2` 第四张鉴权审计表 `auth_audit_log` 的事件范围、追加写规则、索引与对外 DTO 派生约束。 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 a9dbb2d8..914f10c4 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 @@ -336,6 +336,10 @@ server-rs/ - [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) +`wechat_auth_state` 的字段、过期时间、授权场景、callback 单次消费与清理策略,见: + +- [SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md) + ### B. 运行时主状态表 - `runtime_snapshot` diff --git a/docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md b/docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md new file mode 100644 index 00000000..3fedd007 --- /dev/null +++ b/docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md @@ -0,0 +1,324 @@ +# `wechat_auth_state` 表设计 + +日期:`2026-04-21` + +## 1. 文档目的 + +这份文档用于完成 `M2` 的第七条任务:`设计 wechat_auth_state`。 + +目标是把当前只存在于 Node 进程内存中的微信 OAuth `state` 临时仓,升级为一张可跨实例、可过期、可单次消费的 `SpacetimeDB private table`,并固定: + +1. 微信登录 `state` 的职责边界 +2. `wechat/start` 与 `wechat/callback` 的写入/消费顺序 +3. 活跃态、已消费态、已过期态的判定规则 +4. 它与 `auth_identity`、`refresh_session`、`auth_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. 保存微信 `code`、`access_token`、`refresh_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_path`、`request_user_agent` 都属于登录上下文数据 +3. 这张表只服务于 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` | 否 | 发起授权时的 UA 原文快照;缺失时为 `null`。 | +| `expires_at` | `String` | 是 | 当前 `state` 的失效时间,UTC RFC3339。 | +| `consumed_at` | `Option` | 否 | 首次被成功消费的时间;未消费时为 `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. 默认 TTL:`15` 分钟 +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_path`、`scene` 建业务查询索引,因为主链只按 `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_at` 与 `expires_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. 把微信 `code`、`access_token`、用户资料 JSON 直接写进 `wechat_auth_state` +5. 消费成功后立刻硬删除记录,导致无法区分“重复回调”与“从未发起过该 state” +6. 把 `wechat_auth_state` 暴露成前端可直接查询的公共表或订阅面 + +## 14. 本任务完成定义 + +当以下条件满足时,`设计 wechat_auth_state` 视为完成: + +1. 当前 Node 内存状态仓已被完整映射为可落表的短期状态模型。 +2. `wechat/start` 与 `wechat/callback` 的写入、消费、过期、清理规则已固定。 +3. 已和 `auth_identity`、`refresh_session`、`auth_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` diff --git a/server-rs/crates/module-auth/README.md b/server-rs/crates/module-auth/README.md index 3c467986..9b40c737 100644 --- a/server-rs/crates/module-auth/README.md +++ b/server-rs/crates/module-auth/README.md @@ -13,7 +13,7 @@ ## 2. 当前阶段说明 -当前阶段已冻结前六张鉴权基础表设计,剩余 `wechat_auth_state` 与 token 细节仍按顺序继续展开。 +当前阶段已冻结前七张鉴权基础表设计,剩余重点收口在 JWT claims、refresh cookie 与旧接口兼容细节。 后续与本 crate 直接相关的任务包括: @@ -30,6 +30,7 @@ 4. [../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) 5. [../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) 6. [../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) +7. [../../../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md) ## 3. 边界约束