用户体验决定一切
国内 Web 应用的登录方式里,微信扫码已经是事实标准——比手机号验证码更顺、不用记密码、用户基本人手都有微信。
但"微信扫码登录"实际上有好几种不同的方案,对应不同的用户体验和接入条件。新人接这块时常常分不清:
- 有的网站扫码后跳到微信"授权"页面再回来登录
- 有的网站扫码就完事了,不需要二次确认
- 有的扫码是公众号场景,扫完关注还能登录
- 有的是"网站二维码 + 用手机微信扫" → 登录 PC 端
每种背后用的是完全不同的微信能力。本文把三种主流姿势梳理清楚——微信开放平台 OAuth、公众号 OAuth、关注公众号即登录。
一、微信家族里的"应用类型"
要选对登录方案,先要搞清微信家的几类账号:
| 账号类型 | 适用场景 |
|---|
| 微信开放平台应用 | 网站、App 接入"使用微信登录"按钮 |
| 公众号(订阅号) | 内容推送 |
| 公众号(服务号) | 业务对接、可以做 OAuth 登录 |
| 微信小程序 | 小程序生态 |
| 企业微信应用 | 企业内部 |
我们要讲的扫码登录只涉及前两个——开放平台和服务号。
二、方案 A:微信开放平台 OAuth(PC 网站扫码)
最经典的"网页扫码登录"——你在 PC 端访问 12306 / 知乎 / 京东 PC 端,点"微信登录",弹出二维码,手机微信扫一扫确认即可登录。
流程
sequenceDiagram
Browser->>WebSite: 访问登录页
WebSite->>WeChatOpen: redirect 二维码授权 URL
WeChatOpen-->>Browser: 显示二维码
Browser-->>Phone: 用户用手机微信扫
Phone->>WeChatOpen: 扫码 + 确认
WeChatOpen->>WebSite: redirect with code
WebSite->>WeChatOpen: 用 code 换 access_token + openid
WeChatOpen->>WebSite: 返回 token 和用户信息
WebSite-->>Browser: 登录成功,set session接入步骤
1. 在微信开放平台注册"网站应用"
2. 前端跳转授权链接
1
2
3
4
5
6
7
| const url = `https://open.weixin.qq.com/connect/qrconnect`
+ `?appid=${APP_ID}`
+ `&redirect_uri=${encodeURIComponent(CALLBACK)}`
+ `&response_type=code`
+ `&scope=snsapi_login`
+ `&state=${randomState}`;
window.location.href = url;
|
state 是 CSRF 防御——后端要校验回调时的 state 与前端发出的一致。
3. 后端用 code 换 access_token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @GetMapping("/auth/wechat/callback")
public String callback(@RequestParam String code, @RequestParam String state) {
// 校验 state(防 CSRF)
if (!stateService.consume(state)) {
throw new BusinessException("invalid state");
}
// 换 access_token
String url = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=" + APP_ID
+ "&secret=" + APP_SECRET
+ "&code=" + code
+ "&grant_type=authorization_code";
Map resp = restTemplate.getForObject(url, Map.class);
// resp 里有 access_token, openid, unionid
// 拉用户信息
User user = userService.bindOrCreate((String) resp.get("openid"),
(String) resp.get("unionid"));
// 颁发自己的会话 token
return "redirect:" + frontUrl + "?token=" + jwtService.issue(user);
}
|
关键点:openid vs unionid
- openid:用户在这一个应用里的唯一标识——同一用户在你的 A 应用和 B 应用 openid 不同
- unionid:用户在整个开发者账号下的唯一标识——同一开发者下所有应用共享
生产里通常以 unionid 作为主用户标识——避免"同一个微信用户在 PC 端和小程序里看起来是两个账号"。
三、方案 B:公众号 OAuth(关注后授权登录)
很多 H5 应用通过公众号入口登录——比如某些电商小程序的 H5 版本、某些活动落地页。
流程
用户在微信内打开你的网页 → 微信检测到是 H5 → 触发 OAuth → 不需要扫码(已是微信内)→ 用户授权 → 返回业务页面已登录。
sequenceDiagram
User->>WeChat: 打开 H5 链接
WeChat->>WebSite: 加载 H5
WebSite->>OAuth: 跳到 OAuth 授权
Note over OAuth: scope=snsapi_base 静默授权
scope=snsapi_userinfo 弹页面授权
OAuth->>WebSite: 回调 with code
WebSite->>WeChat API: code → openid (+ userinfo)
WebSite->>User: 登录完成snsapi_base vs snsapi_userinfo
公众号 OAuth 有两种 scope,差异很大:
| snsapi_base | snsapi_userinfo |
|---|
| 用户感知 | 无(静默) | 有授权页面 |
| 拿到信息 | 只有 openid | openid + 昵称 + 头像 + 城市等 |
| 适用场景 | 后台关联用户 | 显示头像/昵称 |
优先用 snsapi_base——拿到 openid 后查自己业务库即可,多数场景够用。只有真要展示用户头像昵称时才用 snsapi_userinfo——它会弹一个授权页打断流程。
接入要求
- 服务号(订阅号没有 OAuth 接口)
- 已认证(年费 300)
- 在公众号后台配置"网页授权域名"——只能在配置的域名下使用 OAuth
- 必须放一个微信验证文本到域名根目录
四、方案 C:关注公众号即登录(带场景值二维码)
这是最巧妙的一种——用户扫了带场景值的临时二维码、关注公众号后,公众号收到事件,后端推送登录态给前端。
业务侧体验是:“用户扫码并关注 → PC 端自动登录”——比 OAuth 少一次跳转。
整体架构
sequenceDiagram
Browser->>Backend: 请求登录二维码
Backend->>WeChat: 生成临时二维码(带 sceneId)
WeChat-->>Backend: 返回 ticket
Backend-->>Browser: 二维码图片
Browser->>Browser: 长轮询 / SSE 等待登录
Phone->>WeChat: 用户扫码
WeChat->>Backend: 推送"扫码事件"
(包含 sceneId + openid)
Backend->>Backend: sceneId 与会话关联,缓存 openid
Browser->>Backend: 轮询查询 sceneId 状态
Backend->>Browser: 返回登录信息实现要点
1. 生成带 sceneId 的二维码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 生成临时 sceneId(需要去重 + 过期)
String sceneId = generateUniqueSceneId();
sceneStore.put(sceneId, new SceneState(SceneStatus.PENDING), 5 * 60);
// 调微信接口生成 ticket
Map<String, Object> req = Map.of(
"expire_seconds", 300,
"action_name", "QR_STR_SCENE",
"action_info", Map.of("scene", Map.of("scene_str", sceneId))
);
String ticket = callWeChatApi("/cgi-bin/qrcode/create", req);
// 返回前端:用 ticket 拼二维码 URL
String qrUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
|
2. 接收微信推送
公众号后台配置消息推送 URL,收到事件 callback:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @PostMapping("/wechat/callback")
public String onMessage(@RequestBody String xml) {
WechatEvent event = parseXml(xml);
if ("subscribe".equals(event.getEvent()) // 关注
|| "SCAN".equals(event.getEvent())) { // 已关注扫描
String sceneId = event.getEventKey();
String openid = event.getFromUserName();
// 把扫码用户绑定到 sceneId
sceneStore.update(sceneId, new SceneState(SceneStatus.SCANNED, openid));
// 给用户回个欢迎语
return buildReplyXml(openid, "登录成功!");
}
return "success";
}
|
3. 前端轮询或 SSE
1
2
3
4
5
6
7
8
9
| const interval = setInterval(async () => {
const resp = await fetch(`/auth/scan/check?sceneId=${sceneId}`);
const data = await resp.json();
if (data.status === 'SCANNED') {
localStorage.setItem('token', data.token);
location.href = '/dashboard';
clearInterval(interval);
}
}, 1500);
|
更优雅的姿势用 SSE 替代轮询——服务端扫码后主动推送。
这套方案的限制
- 要服务号,订阅号不行
subscribe 事件只在关注时触发——已关注用户扫码触发的是 SCAN,业务要分两种处理- 临时二维码 5 分钟过期——前端要管理超时
- 场景值长度限制——临时二维码 scene_str 最多 64 字符
五、三种方案对比
| 开放平台 OAuth | 公众号 OAuth | 公众号扫码即登录 |
|---|
| 适用场景 | PC 网站 | H5(微信内打开) | PC 网站(关注公众号) |
| 资质 | 开放平台账号 + 企业 | 服务号 + 认证 | 服务号 + 认证 |
| 用户体验 | 扫码 → 授权确认 → 登录 | 自动跳转(snsapi_base 无感) | 扫码 → 关注 → 登录 |
| 是否需关注 | 否 | 否 | 是 |
| 跳转步骤 | 多 | 一次 | 无(PC 端不跳) |
| 适合做营销 | 否 | 是 | 是(强行涨粉) |
六、实战建议
选型
flowchart TD
Start([场景?])
Start --> A{PC 网站登录?}
A -->|是| B{要不要涨粉?}
B -->|不要| O1[开放平台 OAuth]
B -->|要涨粉| C[公众号扫码即登录]
A -->|否,H5 微信内| O2[公众号 OAuth]共性踩坑
1. 多账号体系
用户可能从 H5 / PC / 小程序多个端登录——用 unionid 统一身份,否则会有重复账号。
2. AppSecret 必须保密
永远不能暴露给前端——只在服务端用。前端拿到 code 后调自己后端,由后端去换 access_token。
3. 频率限制
微信 API 有调用频率限制(每天 N 次、每分钟 M 次),超额会被拒。生产要做:
- access_token 缓存(2 小时有效,重复调用会被踢掉旧 token)
- API 调用打日志,超频告警
4. 二维码缓存
每次生成新二维码都要调微信 API——前端每次刷新页面都生成新的会撞限制。建议给二维码加 1-2 分钟的客户端缓存。
5. 异常处理
- 用户在授权页面"取消"——回调还是会被调用,参数有差异
- code 重复使用——只能用一次
- access_token 过期——要刷新
6. 测试环境验证
微信不能跨域名调试——要么把测试环境配成独立公众号,要么用内网穿透。生产前一定在真实公众号下测过完整流程。
七、一些前沿姿势
微信小程序登录
如果你的业务要跨小程序 + H5 + PC 共享用户体系:
- 小程序:
wx.login() + code2Session 拿 openid + unionid - H5:snsapi_base
- PC:开放平台 OAuth
用 unionid 一统身份——前提是这些应用都挂在同一个开放平台账号下,否则 unionid 不互通。
跨端会话同步
PC 扫码登录后,移动端怎么也保持同步?通常是:
- PC 拿到 token 后调一个"标记 unionid 已登录"接口
- App / H5 启动时检查 unionid 是否处于"已登录"状态——是就静默登录
这是 12306 / 京东这类多端应用的常用做法。
小结
把全文压一句:
PC 网站用开放平台 OAuth;H5 微信内用公众号 OAuth;想顺带涨粉用『关注公众号即登录』。三者底层协议都是微信生态,但接入门槛、体验、营销价值差别很大。
工程上记住几条:
- AppSecret 永远在服务端
- 用 unionid 统一身份
- access_token 缓存 + 频率告警
- state 防 CSRF
- 二维码加客户端缓存避免刷限额
把这套吃透,国内 Web 应用的"登录"问题基本就解决了。