# 微信登录真实联调手册 日期:`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_JS_CODE_SESSION_ENDPOINT` | 否 | 默认小程序 `jscode2session` 接口 | | `WECHAT_MINI_PROGRAM_APP_ID` | 小程序 `real` 模式必填 | 微信小程序 AppID;不填时回退 `WECHAT_APP_ID` | | `WECHAT_MINI_PROGRAM_APP_SECRET` | 小程序 `real` 模式必填 | 微信小程序 AppSecret;不填时回退 `WECHAT_APP_SECRET` | | `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. 小程序 web-view 登录联调步骤 小程序壳走原生 `wx.login`,不走网页 OAuth callback。联调前需要额外确认: ```bash WECHAT_AUTH_ENABLED=true WECHAT_AUTH_PROVIDER=real WECHAT_MINI_PROGRAM_APP_ID="你的微信小程序 AppID" WECHAT_MINI_PROGRAM_APP_SECRET="你的微信小程序 AppSecret" ``` 在 `miniprogram/config.js` 中确认: ```js const WEB_VIEW_ENTRY_URL = 'https://你的H5业务域名/'; const API_BASE_URL = 'https://你的服务器域名/'; const MINI_PROGRAM_APP_ID = '你的微信小程序 AppID'; ``` 联调流程: 1. 微信开发者工具打开项目根目录。 2. 小程序启动后调用 `wx.login`。 3. 小程序壳请求: ```http POST /api/auth/wechat/miniprogram-login ``` 4. 后端通过 `jscode2session` 兑换 `openid/unionid`。 5. 后端返回系统 `token`、`bindingStatus` 与 `user`。 6. 小程序壳打开 H5,并在 hash 中附加: - `auth_provider=wechat` - `auth_token=...` - `auth_binding_status=active|pending_bind_phone` 7. H5 消费 hash 后通过 `/api/auth/me` 恢复登录态。 这里不能把裸 `openid` 作为 web-view query 登录凭证;`openid` 只能留在后端身份绑定层,H5 只消费本系统 JWT。 ## 9. 账号命中规则 当前实现固定按以下顺序命中已有账号: 1. 先按 `unionid` 2. 再按 `openid` 3. 都没有命中时,创建 `pending_bind_phone` 微信壳账号 补充规则: 1. 若按 `unionid` 命中了已有微信身份,但本次微信回调带来了新的 `openid`,后端会把新的 `openid -> user_id` 映射补齐 2. 若后续绑定手机号时发现该手机号已经属于正式账号,则会把微信身份并入这个正式账号 ## 10. 前端验收点 前端联调时至少检查以下行为: 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 联调。