init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -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 的统一登录态。**