This commit is contained in:
2026-04-21 19:17:31 +08:00
parent d234d27cc0
commit 89129ef1f4
83 changed files with 13329 additions and 176 deletions

View File

@@ -0,0 +1,317 @@
# 微信登录真实联调手册
日期:`2026-04-21`
## 1. 文档目的
这份文档用于把当前仓库里的微信登录,从“代码已具备 mock / real 双模式”推进到“开发、测试、部署都能按步骤联调”的级别。
文档目标不是重复实现设计,而是回答以下落地问题:
1. 本地先怎么用 mock 跑通
2. 什么时候切 `real`
3. 真实微信开放平台需要配什么
4. 回调地址到底怎么拼
5. 前后端各自怎么验证
6. 失败时先查哪一层
## 2. 当前实现结论
当前仓库里的微信登录实现固定为两段式:
1. 微信 OAuth 只负责返回第三方身份:`code -> openid / unionid`
2. Rust `api-server` 负责把该第三方身份换成本系统 JWT
因此当前正式口径固定为:
1. 前端不直接消费微信 token
2. `SpacetimeDB` 未来也不直接消费微信 token
3. 统一由 `api-server` 签发系统 JWT再回给前端和后续服务层
## 3. 模式说明
### 3.1 mock 模式
适用场景:
1. 本地前后端先验证交互链路
2. 还没有微信开放平台配置
3. 还没有可用公网回调域名
当前配置:
```env
WECHAT_AUTH_ENABLED="true"
WECHAT_AUTH_PROVIDER="mock"
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
WECHAT_REDIRECT_PATH="/"
WECHAT_MOCK_USER_ID="wx-mock-user"
WECHAT_MOCK_UNION_ID="wx-mock-union"
WECHAT_MOCK_DISPLAY_NAME="微信旅人"
WECHAT_MOCK_AVATAR_URL=""
```
mock 模式行为固定为:
1. `GET /api/auth/wechat/start` 仍会创建一次性 `state`
2. 返回的 `authorizationUrl` 不会跳去微信,而是直接指向本地 callback
3. callback 会走 mock 身份:`mock_code + state`
4. 后端仍会完整创建微信登录态、refresh session 与待绑定账号
### 3.2 real 模式
适用场景:
1. 已拿到微信开放平台应用
2. 已有可访问的公网域名
3. 已在微信开放平台后台配置回调域名
当前配置:
```env
WECHAT_AUTH_ENABLED="true"
WECHAT_AUTH_PROVIDER="real"
WECHAT_APP_ID="你的微信开放平台 AppID"
WECHAT_APP_SECRET="你的微信开放平台 AppSecret"
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
WECHAT_REDIRECT_PATH="/"
```
real 模式行为固定为:
1. `GET /api/auth/wechat/start` 返回微信真实授权地址
2. 用户在微信侧完成授权
3. 微信回跳到 `WECHAT_CALLBACK_PATH`
4. 后端用 `code``access_token/openid/unionid`
5. 后端签发本系统 JWT并把结果通过 URL hash 回跳给前端
## 4. 环境变量清单
当前真实联调至少需要这些变量:
| 变量 | 必填 | 说明 |
| --- | --- | --- |
| `WECHAT_AUTH_ENABLED` | 是 | 是否启用微信登录 |
| `WECHAT_AUTH_PROVIDER` | 是 | `mock``real` |
| `WECHAT_APP_ID` | `real` 模式必填 | 微信开放平台应用 ID |
| `WECHAT_APP_SECRET` | `real` 模式必填 | 微信开放平台应用 Secret |
| `WECHAT_CALLBACK_PATH` | 是 | 回调路径,默认 `/api/auth/wechat/callback` |
| `WECHAT_REDIRECT_PATH` | 是 | 登录完成后前端默认落点 |
| `WECHAT_AUTHORIZE_ENDPOINT` | 否 | 默认桌面二维码授权地址 |
| `WECHAT_ACCESS_TOKEN_ENDPOINT` | 否 | 默认 access_token 接口 |
| `WECHAT_USER_INFO_ENDPOINT` | 否 | 默认用户信息接口 |
| `WECHAT_STATE_TTL_MINUTES` | 否 | state 有效期,默认 `15` 分钟 |
补充说明:
1. `WECHAT_CALLBACK_PATH` 只是路径,不是完整 URL。
2. 当前完整回调地址由后端在运行时按请求头拼接:
- 优先 `x-forwarded-proto`
- 其次 `host` / `x-forwarded-host`
3. 因此部署到反向代理后,代理层必须正确透传 `host``x-forwarded-proto`
## 5. 微信开放平台后台配置
真实联调前,需要先在微信开放平台完成以下配置:
1. 创建网站应用并拿到 `AppID / AppSecret`
2. 配置网站授权回调域名
3. 确保回调域名与实际访问域名一致
当前项目必须特别注意:
1. 微信后台通常配置的是“域名”,不是完整路径
2. 但项目真正收到回调时会落到:
```text
https://你的域名/api/auth/wechat/callback
```
3. 如果代理层把 HTTPS 终止在上游网关,必须把 `x-forwarded-proto=https` 正确传给 `api-server`
4. 否则后端可能生成 `http://.../api/auth/wechat/callback`,导致微信侧回调地址不匹配
## 6. 本地 mock 联调步骤
### 6.1 配置
`.env.local` 中写入:
```env
SMS_AUTH_ENABLED="true"
WECHAT_AUTH_ENABLED="true"
WECHAT_AUTH_PROVIDER="mock"
VITE_AUTH_ALLOW_DEV_GUEST="false"
```
### 6.2 启动
前端和后端分别启动当前项目自己的开发链路。
若只验证 Rust 后端,可直接启动 `server-rs``api-server`
### 6.3 验证顺序
1. 请求 `GET /api/auth/login-options`
2. 期望返回同时包含:
- `phone`
- `wechat`
3. 前端点击“微信登录”
4. 浏览器应先跳到 `/api/auth/wechat/start`
5. 随后直接命中本地 `/api/auth/wechat/callback?...`
6. 前端收到 hash
- `auth_provider=wechat`
- `auth_token=...`
- `auth_binding_status=pending_bind_phone`
7. 页面进入“绑定手机号”界面
8. 发送验证码并绑定手机号
9. 若手机号未使用,当前账号激活
10. 若手机号已对应正式账号,微信身份并入已有正式账号
## 7. 真实微信联调步骤
### 7.1 切换配置
`.env.local` 中切到:
```env
SMS_AUTH_ENABLED="true"
WECHAT_AUTH_ENABLED="true"
WECHAT_AUTH_PROVIDER="real"
WECHAT_APP_ID="你的 AppID"
WECHAT_APP_SECRET="你的 AppSecret"
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
WECHAT_REDIRECT_PATH="/"
VITE_AUTH_ALLOW_DEV_GUEST="false"
```
### 7.2 部署要求
需要有一个用户浏览器能够访问的地址,例如:
```text
https://game.example.com
```
并且该地址最终把认证请求转发到当前 Rust `api-server`
### 7.3 代理层要求
反向代理必须至少透传:
1. `Host`
2. `X-Forwarded-Proto`
3. `X-Forwarded-Host`(如果你的代理链路会改写 Host
### 7.4 验证顺序
1. 浏览器访问前端页面
2. 点击“微信登录”
3. 观察 `/api/auth/wechat/start` 返回的 `authorizationUrl`
4. 确认其中 `redirect_uri` 指向真实回调地址
5. 在微信授权完成后,确认请求回到:
```text
/api/auth/wechat/callback?code=...&state=...
```
6. 观察回跳前端地址是否带上:
- `auth_provider=wechat`
- `auth_token=...`
- `auth_binding_status=active|pending_bind_phone`
7. 如果进入待绑定页面,继续完成手机号绑定
8. 绑定后再请求 `GET /api/auth/me`
9. 确认:
- 当前用户存在
- `wechatBound = true`
- `bindingStatus` 已更新为目标状态
## 8. 账号命中规则
当前实现固定按以下顺序命中已有账号:
1. 先按 `unionid`
2. 再按 `openid`
3. 都没有命中时,创建 `pending_bind_phone` 微信壳账号
补充规则:
1. 若按 `unionid` 命中了已有微信身份,但本次微信回调带来了新的 `openid`,后端会把新的 `openid -> user_id` 映射补齐
2. 若后续绑定手机号时发现该手机号已经属于正式账号,则会把微信身份并入这个正式账号
## 9. 前端验收点
前端联调时至少检查以下行为:
1. 登录弹窗只在 `login-options` 返回 `wechat` 时显示“微信登录”按钮
2. 点击微信登录后应跳去后端返回的 `authorizationUrl`
3. 回调 hash 被前端消费后,应把 `auth_token` 存入本地登录态
4.`auth_binding_status=pending_bind_phone`,页面必须进入绑定手机号界面
5. 绑定成功后,应切回正常已登录状态
## 10. 后端验收点
当前后端至少应满足以下检查:
1. `GET /api/auth/wechat/start` 能返回授权地址
2. `GET /api/auth/wechat/callback` 能创建系统会话并回跳
3. `POST /api/auth/wechat/bind-phone` 能完成补绑
4. `GET /api/auth/me` 能反映最新 `bindingStatus / wechatBound`
5. access token claims 中的 `provider` 应保持当前会话来源为 `wechat`
## 11. 常见失败点
### 11.1 点微信登录后直接报“微信登录暂未启用”
先检查:
1. `WECHAT_AUTH_ENABLED` 是否为 `true`
2. 运行的是否真是 Rust `api-server`
3. 前端是否还在打旧 Node 服务
### 11.2 微信授权页能打开,但回调报错
先检查:
1. 微信后台配置的回调域名是否正确
2. 代理层是否正确透传 `host``x-forwarded-proto`
3. `WECHAT_CALLBACK_PATH` 是否与当前代码路径一致
### 11.3 按 `unionid` 命中后又出现新壳账号
先检查:
1. 微信开放平台当前应用是否真的能返回稳定 `unionid`
2. 当前账号历史上是否只有 `openid` 没有 `unionid`
3. 是否发生了不同开放平台主体之间的数据割裂
### 11.4 绑定手机号后命中了已有正式账号,但前端看到 `loginMethod=phone`
这是当前实现允许的结果。
原因是:
1. 返回的是目标正式账号快照
2. 目标正式账号本身的主登录方式仍可能是 `phone`
3. 但当前会话签发的 access token `provider` 仍然是 `wechat`
## 12. 当前自动化验证证据
当前仓库里已经有这些自动化验证:
1. `cargo test -p api-server`
覆盖:
- `wechat/start`
- `wechat/callback`
- `wechat/bind-phone`
2. `cargo test -p module-auth`
覆盖:
- `unionid` 优先命中已有微信用户
- 微信待绑定账号并入已有手机号正式账号
3. `npm test -- --run src/services/authService.test.ts`
覆盖:
- 前端微信登录起跳
- callback hash 消费
## 13. 一句话切换原则
本地先用 `mock` 跑通页面和会话闭环,公网域名与微信后台配置就绪后,再切 `real` 做真实 OAuth 联调。