# Q's blog > Bsheepcoder 的技术博客,记录 AI、编程、计算机科学的学习笔记 --- # 微信登录统一接入:扫码登录 + 小程序登录的安全架构实战 Date: 2026-07-03 | Tags: 安全, 前端, 微信登录, OAuth2, Vue3, Spring Boot | URL: https://bsheepcoder.github.io/2026/07/03/frontend-wechat-unified-login/ ## 一、元认知:微信登录到底在解决什么问题 > 登录的本质不是"让用户进来",而是"确认这个人是谁"。 这句话看似废话,但它揭示了微信登录体系最令人困惑的设计:**同一个用户,在公众号、小程序、网站应用、移动应用中有四个不同的 openid**。如果你不理解为什么,后面的实现做得再好也是空中楼阁。 ### 1.1 openid 的分裂:设计哲学而非技术缺陷 微信的 openid 是**应用维度**的,不是**用户维度**的。一个用户在你的小程序里是 `oxABC123`,在你的网站应用里是 `oxDEF456`,在你的公众号里是 `oxGHI789`——三个完全不同的字符串,指向同一个人。 为什么这么设计?**隔离性**。每个应用只能看到自己的 openid,无法跨应用追踪用户。这是隐私保护的第一道防线:你的小程序无法知道用户在另一个小程序里是谁。 但业务需要打通。所以微信提供了 **UnionID 机制**:只要多个应用绑定到同一个微信开放平台账号,就能获取一个全局唯一的 `unionid`,作为用户身份的统一标识。 ``` 用户(张三) ├── 小程序 openid: oxAAA ──┐ ├── 网站应用 openid: oxBBB ──┼── 都绑定到同一个开放平台 → unionid: ou_唯一 └── 公众号 openid: oxCCC ──┘ ``` > **小而美原则**:如果你只有一个应用(纯小程序或纯网站),不需要 UnionID,直接用 openid 就够了。只有当你的产品矩阵需要跨端打通时,才值得引入 UnionID。 ### 1.2 三种登录方式的本质 微信提供的登录能力可以归为三类: | 方式 | 本质 | 适用场景 | 前提条件 | |------|------|---------|---------| | 网页扫码登录 | OAuth2(开放平台) | PC 网站、H5(非微信内) | 开放平台注册「网站应用」 | | 小程序登录 | 微信私有协议 | 微信小程序 | 小程序 AppID | | 网页授权登录 | OAuth2(服务号) | 公众号 H5 页面 | 已认证服务号 | 本文聚焦前两种——它们是"小而美"产品最常见的组合:**PC 端扫码 + 小程序端一键登录**。 --- ## 二、搭积木:统一认证服务架构 ### 2.1 数据库设计:一张表搞定 不需要两张表、三张表。一张 `user` 表,核心字段只有三个: | 字段 | 类型 | 说明 | |------|------|------| | `unionid` | VARCHAR(64) UNIQUE | 微信统一标识,可为空 | | `web_openid` | VARCHAR(64) | 网站应用的 openid | | `mini_openid` | VARCHAR(64) | 小程序的 openid | **设计决策**:`unionid` 允许为空。如果用户只用过小程序登录,且小程序没有绑定开放平台,就没有 unionid。用 openid 也能正常工作,只是无法跨端打通。 其余字段按需添加:`nickname`、`avatar_url`、`phone`、`created_at`、`last_login_at`、`last_login_src`(记录最近登录来源,方便分析)。 索引策略: - `unionid` 唯一索引(主查询路径) - `web_openid` 普通索引(兜底查询) - `mini_openid` 普通索引(兜底查询) ### 2.2 后端架构(Spring Boot) ``` com.example.auth ├── controller/AuthController ← API 端点(5个接口) ├── service/WechatWebAuthService ← 网页扫码登录核心逻辑 ├── service/WechatMiniAuthService ← 小程序登录核心逻辑 ├── security/JwtTokenProvider ← JWT 签发/验证 └── config/WechatProperties ← 微信配置(appid/secret) ``` 配置结构(`application.yml`): ```yaml wechat: web: app-id: ${WECHAT_WEB_APP_ID} # 开放平台网站应用 AppID app-secret: ${WECHAT_WEB_APP_SECRET} redirect-uri: https://yourdomain.com/api/auth/wechat/web/callback mini: app-id: ${WECHAT_MINI_APP_ID} # 小程序 AppID app-secret: ${WECHAT_MINI_APP_SECRET} jwt: secret: ${JWT_SECRET} # 至少 256 bit access-token-expire: 7200 # 2 小时 refresh-token-expire: 2592000 # 30 天 ``` > **安全原则**:所有密钥通过环境变量注入,绝不硬编码。Spring Boot 的 `@ConfigurationProperties` 自动绑定,配合 Docker/K8s 的 secret 管理,密钥不出现在代码和配置文件中。 --- ## 三、案例即原理:核心实现步骤 ### 3.1 网页扫码登录:六步完成 > **端点选择**:开放平台网站应用用 `connect/qrconnect`(scope=`snsapi_login`),服务号网页授权用 `connect/oauth2/authorize`(scope=`snsapi_userinfo`)。两者都能扫码,但前者是 PC 端专用,后者仅限微信内置浏览器。本文用前者。 网页扫码有两种呈现方式,选哪种取决于你的产品需求: | 方式 | 体验 | 适用场景 | |------|------|---------| | 跳转式 | 跳到微信页面扫码,再跳回来 | 简单快速,适合 MVP | | 内嵌式 | 二维码直接嵌在你的页面里 | 体验更好,适合正式产品 | **跳转式流程**(六步): ``` 浏览器 你的后端 微信服务器 │ │ │ │ 1. 点击"微信登录" │ │ │ ──────────────────────> │ │ │ │ │ │ 2. 302 重定向到微信授权页 │ │ │ <────────────────────── │ │ │ │ │ │ 3. 用户扫码确认 │ │ │ ─────────────────────────────────────────────────> │ │ │ │ │ 4. 微信回调 redirect_uri?code=xxx&state=yyy │ │ <───────────────────────────────────────────────── │ │ │ │ │ 5. 前端将 code 发到后端 │ │ │ ──────────────────────> │ │ │ │ 6. 用 code 换 access_token │ │ │ ────────────────────────> │ │ │ <──────────────────────── │ │ │ │ │ │ 7. 查找/创建用户,签发 JWT │ │ 8. 返回 JWT │ │ │ <────────────────────── │ │ ``` **每一步做什么**: **第一步:前端生成 state 并跳转** 前端生成一个随机字符串作为 `state`(防 CSRF),存入 `sessionStorage`,然后将用户重定向到后端的 `/api/auth/wechat/web/login?state=xxx`。 后端收到后,将 `state` 存入 Redis(设 5 分钟过期),然后 302 重定向到微信授权页: ``` https://open.weixin.qq.com/connect/qrconnect ?appid=你的网站应用AppID &redirect_uri=你的回调地址(需 URL 编码) &response_type=code &scope=snsapi_login &state=刚才的随机字符串 #wechat_redirect ``` **第二步:用户扫码授权** 用户在微信中确认授权后,微信会将浏览器重定向到你配置的 `redirect_uri`,并附带 `code` 和 `state` 参数。 **第三步:后端校验 state 并换取 token** 后端收到回调后,先检查 Redis 中是否存在这个 `state`——存在则删除(一次性使用),不存在则拒绝请求(可能是 CSRF 攻击)。 校验通过后,用 `code` 调用微信接口换取 `access_token`: ``` GET https://api.weixin.qq.com/sns/oauth2/access_token ?appid=你的网站应用AppID &secret=你的网站应用Secret &code=回调带回的code &grant_type=authorization_code ``` 返回结果包含:`access_token`(用户授权凭证)、`openid`(该用户在你网站应用中的标识)、`unionid`(如果已绑定开放平台)。 **第四步:查找或创建用户** 用 `unionid`(优先)或 `web_openid` 查询数据库: - 找到 → 更新 `last_login_at`,补充缺失的 `unionid` - 没找到 → 插入新用户记录 **第五步:签发 JWT** 生成两个 Token: - `access_token`:有效期 2 小时,用于接口认证 - `refresh_token`:有效期 30 天,用于刷新 access_token **第六步:前端存储 Token** 前端收到 JWT 后存入 `localStorage`(或 HttpOnly Cookie),后续请求通过 `Authorization: Bearer ` 头携带。 **内嵌式方案**(推荐用于正式产品): 跳转式的体验不好——用户被带离你的页面,扫码后又跳回来,中间有白屏闪烁。微信提供了内嵌二维码方案,用户始终留在你的页面上。 步骤很简单: 1. 在页面中引入微信提供的 JS 文件:`http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js` 2. 在登录区域创建一个容器 `
` 3. 实例化 `WxLogin` 对象,传入你的 appid、scope、redirect_uri 等参数 二维码会自动渲染到容器中。用户扫码授权后,微信通过 **JS 跨域回调**将 `code` 返回给当前页面(而非跳转 redirect_uri),前端直接拿到 `code` 发给后端。 > **本地开发注意**:内嵌式方案的 `redirect_uri` 必须与当前页面同域。如果你在 `localhost:5173` 开发,redirect_uri 也必须是 `localhost:5173` 下的路径。这意味着后端回调接口需要在前端同域下,或者用 Vite proxy 转发。 ### 3.2 小程序登录:三步完成 小程序登录比网页扫码简单得多——用户无感知,后台静默完成。 **第一步:小程序端调用 wx.login()** ```javascript wx.login({ success(res) { // res.code 是一次性凭证,有效期 5 分钟 // 将 code 发送到你的后端 } }) ``` **第二步:后端用 code 换取用户标识** 调用微信接口: ``` GET https://api.weixin.qq.com/sns/jscode2session ?appid=你的小程序AppID &secret=你的小程序Secret &js_code=前端传来的code &grant_type=authorization_code ``` 返回结果包含:`session_key`(会话密钥,用于解密敏感数据)、`openid`(小程序维度的用户标识)、`unionid`(如果已绑定开放平台)。 > **安全红线**:`session_key` 绝不能返回给前端。它是解密 `encryptedData` 的密钥,泄露等于用户数据裸奔。只存在后端,建议存 Redis。 **第三步:查找或创建用户 + 签发 JWT** 逻辑与网页登录完全一致:用 `unionid` 或 `mini_openid` 查找用户,签发双 Token。 ### 3.3 手机号获取:新版 vs 旧版 小程序获取手机号有两种方式,2023 年后微信改了接口: **旧版(已废弃,但存量代码仍大量存在)**: - 前端获取 `encryptedData` + `iv` - 后端用 `session_key` 做 AES-128-CBC 解密 - 问题:`session_key` 需要在后端存储和传输,增加了泄露风险 **新版(推荐)**: - 前端点击 `