# 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. 常见返回物是 `code`、`access_token`、`openid`、`unionid`,不是可直接按 OIDC `iss/sub/aud` 校验的标准 JWT。 3. `SpacetimeDB` 当前直接接受的是 OIDC 兼容 JWT,而不是微信的 provider 原始返回。 因此正式口径固定为: - 微信只负责提供三方身份。 - 系统内部登录态统一由 `Axum` 签发。 - 未来接入 `SpacetimeDB` 时,传给 `.withToken(...)` 的仍然是本系统 JWT。 ### 2.2 与 SpacetimeDB 的关系 本阶段虽然前端仍然不直连 `SpacetimeDB`,但 JWT 语义必须从现在就和后续保持一致: 1. `iss` 固定为本系统网关发行者。 2. `sub` 固定为系统内稳定用户 ID,不允许使用手机号、`openid`、`unionid`。 3. `provider` 表示**当前会话登录来源**,允许为 `password`、`phone`、`wechat`。 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`。 ### 3.4 `POST /api/auth/wechat/miniprogram-login` 职责固定为: 1. 接收微信小程序原生壳通过 `wx.login` 拿到的 `code`。 2. 在 `Axum` 内调用微信 `jscode2session`,兑换 `openid/unionid`。 3. 复用 `resolve_login` 处理 `unionid/openid -> user_id` 的查找、补写和待绑定账号创建。 4. 签发本系统 access token,并创建 refresh session。 5. 返回: - `token` - `bindingStatus` - `user` 关键约束: 1. 小程序壳不能把裸 `openid` 直接拼给 H5 做登录。 2. H5 仍只消费本系统 `auth_token`,小程序壳只是把这枚 token 放入既有 hash 回调格式。 3. 小程序请求必须补传 `x-client-type=mini_program` 与 `x-client-runtime=wechat_mini_program`,用于 refresh session 记录来源。 ## 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` 7. `POST /api/auth/wechat/miniprogram-login` ## 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_JS_CODE_SESSION_ENDPOINT` 11. `WECHAT_MINI_PROGRAM_APP_ID` 12. `WECHAT_MINI_PROGRAM_APP_SECRET` 13. `WECHAT_STATE_TTL_MINUTES` 14. `WECHAT_MOCK_USER_ID` 15. `WECHAT_MOCK_UNION_ID` 16. `WECHAT_MOCK_DISPLAY_NAME` 17. `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 的统一登录态。**