Files
Genarrative/docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md
2026-04-21 19:17:31 +08:00

5.6 KiB
Raw Blame History

Axum 微信登录接入设计

日期:2026-04-21

1. 文档目的

这份文档用于把 Rust api-server 当前阶段的微信登录落地边界固定到可编码级别,避免在接入 SpacetimeDB 前把“第三方 provider 身份”和“系统内登录态 JWT”混成一层。

本次设计固定以下结论:

  1. 微信不能作为 SpacetimeDB 直连时直接消费的 OIDC provider。
  2. 微信返回的 code -> access_token/openid/unionid 只能先在 Axum 侧完成兑换。
  3. Axum 在拿到微信身份后,签发本系统自己的 OIDC 兼容 JWT
  4. 前端、Axum、未来 SpacetimeDB 都统一消费这枚系统 JWT而不是直接消费微信 token。

2. 核心边界

2.1 为什么不能直接把微信 token 给 SpacetimeDB

原因固定为:

  1. 当前微信开放平台登录链路不是标准 OIDC id_token 语义。
  2. 常见返回物是 codeaccess_tokenopenidunionid,不是可直接按 OIDC iss/sub/aud 校验的标准 JWT。
  3. SpacetimeDB 当前直接接受的是 OIDC 兼容 JWT而不是微信的 provider 原始返回。

因此正式口径固定为:

  • 微信只负责提供三方身份。
  • 系统内部登录态统一由 Axum 签发。
  • 未来接入 SpacetimeDB 时,传给 .withToken(...) 的仍然是本系统 JWT。

2.2 与 SpacetimeDB 的关系

本阶段虽然前端仍然不直连 SpacetimeDB,但 JWT 语义必须从现在就和后续保持一致:

  1. iss 固定为本系统网关发行者。
  2. sub 固定为系统内稳定用户 ID不允许使用手机号、openidunionid
  3. provider 表示当前会话登录来源,允许为 passwordphonewechat
  4. binding_status 表示当前账号是否仍处于 pending_bind_phone

3. 登录主链

3.1 GET /api/auth/wechat/start

职责固定为:

  1. 归一化 redirectPath
  2. User-Agent 判断授权场景:
    • desktop
    • wechat_in_app
  3. 创建一次性 state
  4. 返回微信授权地址

关键约束:

  1. 普通手机浏览器且非微信内打开时,不创建 state直接报错。
  2. 每次点击微信登录都创建新 state不复用旧 state。

3.2 GET /api/auth/wechat/callback

职责固定为:

  1. 先消费一次性 state
  2. 再用 code 或 mock code 换取微信身份
  3. 先按 unionid,再按 openid 查系统内是否已有绑定账号
  4. 若没有账号,则创建 pending_bind_phone 的微信壳账号
  5. 若本次是按 unionid 命中,但微信回调带回了新的 openid,必须把新的 openid -> user_id 映射一并回写
  6. 签发系统 access token
  7. 创建 refresh session
  8. 以 hash 片段回跳前端:
    • auth_provider=wechat
    • auth_token=...
    • auth_binding_status=active|pending_bind_phone

3.3 POST /api/auth/wechat/bind-phone

职责固定为:

  1. 仅允许当前 Bearer 用户为 pending_bind_phone
  2. 校验手机号验证码
  3. 若手机号已对应正式账号:
    • 把微信身份归并到该正式账号
    • 删除当前微信壳账号
  4. 若手机号尚未绑定:
    • 激活当前微信账号
    • 写入手机号与掩码
  5. 再次签发 access token 与 refresh session

补充约束:

  1. 若绑定的是当前待绑定微信账号本体,则返回用户快照的 loginMethod = wechat
  2. 若是并入已有手机号正式账号,则返回目标正式账号快照,当前实现会保持其账号主登录方式,例如 loginMethod = phone
  3. 但 access token 中的 provider 仍按本次登录来源签发,不依赖账号主登录方式推断,因此微信绑定后的当前会话仍会签发 provider = wechat

4. 当前最小实现策略

当前阶段为了先打通 Rust 后端闭环,采用以下最小实现:

  1. module-auth 内使用进程内存仓模拟:
    • 用户
    • refresh session
    • 微信 identity
    • 微信 state
    • 手机验证码
  2. 短信验证码先走 mock
    • 固定验证码 123456
    • TTL 5 分钟
    • 冷却 60
  3. 微信 provider 同时支持:
    • mock
    • real

这意味着:

  1. 本轮目的是先把 Rust 认证主链补齐。
  2. 后续切到真实 SpacetimeDB private table 时,保留接口 contract不改前端页面。

5. 当前接口影响面

本次接入会补齐这些 Rust 接口:

  1. GET /api/auth/login-options
  2. POST /api/auth/phone/send-code
  3. POST /api/auth/phone/login
  4. GET /api/auth/wechat/start
  5. GET /api/auth/wechat/callback
  6. POST /api/auth/wechat/bind-phone

6. 环境变量

当前 Rust api-server 需要新增或明确这些变量:

  1. WECHAT_AUTH_ENABLED
  2. WECHAT_AUTH_PROVIDER
  3. WECHAT_APP_ID
  4. WECHAT_APP_SECRET
  5. WECHAT_CALLBACK_PATH
  6. WECHAT_REDIRECT_PATH
  7. WECHAT_AUTHORIZE_ENDPOINT
  8. WECHAT_ACCESS_TOKEN_ENDPOINT
  9. WECHAT_USER_INFO_ENDPOINT
  10. WECHAT_STATE_TTL_MINUTES
  11. WECHAT_MOCK_USER_ID
  12. WECHAT_MOCK_UNION_ID
  13. WECHAT_MOCK_DISPLAY_NAME
  14. WECHAT_MOCK_AVATAR_URL

7. 与后续 SpacetimeDB 的衔接要求

未来把认证真相从内存仓切到 SpacetimeDB 时,必须继续保持:

  1. wechat_auth_state 只做一次性 OAuth 状态,不存 provider token。
  2. auth_identity 负责 openid/unionid -> user_id 绑定。
  3. refresh_session 负责设备会话。
  4. Axum 负责签 JWT。
  5. SpacetimeDB module 只信系统 JWT claims不直接解析微信回调结果。

8. 一句话结论

微信登录在本项目中的正确接法不是“把微信 token 直接接到 SpacetimeDB”,而是:

微信只提供三方身份Axum 负责完成 provider 兑换并签发系统 OIDC 兼容 JWT再把这枚 JWT 用作前端与未来 SpacetimeDB 的统一登录态。