This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# 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`。
|
||||
|
||||
## 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 的统一登录态。**
|
||||
Reference in New Issue
Block a user