微信网页扫码登录全链路技术文档
微信网页扫码登录全链路技术文档
一、 核心概念与角色
- 临时票据 (Code):用户扫码授权后,微信发给系统的临时凭证(有效期极短,且只能用一次)。
- 授权状态 (State):由系统生成的随机字符串,用于关联“发起请求”和“接收回调”,防止 CSRF 攻击。
- 回调地址 (Redirect URI):用户扫码后,浏览器重定向的目标。
二、 二维码生成方式对比
生成方式决定了用户的视觉体验,但不决定后端逻辑。
| 生成方式 | 实现机制 | 优点 | 缺点 |
|---|---|---|---|
| JS 脚本 (内嵌式) | 引入 wxLogin.js,在页面指定 div 中渲染 iframe。 |
用户无需离开当前页面,体验平滑。 | 需要处理 iframe 样式兼容性。 |
| 后端构造 URL (跳转式) | 后端拼接授权链接,前端执行 window.location.href。 |
简单直接,适用于移动端 H5 或独立登录页。 | 用户会跳离当前网站。 |
三、 方案 A 与 方案 B 的本质区别
这是架构设计的核心选择,决定了 code 的归宿。
1. 方案 A:直接回调后端 (推荐)
- 路径:微信服务器 -> 后端接口 (Spring Security 拦截)。
- 前端角色:仅作为“结果接收者”。
- 实现逻辑:
redirect_uri指向后端(如/login/oauth2/code/wechat)。- Spring Security 自动获取
code并换取token。 - 后端通过
successHandler执行response.sendRedirect将自定义 Token 带回前端。
- 适用场景:追求高安全规范、希望利用 Spring Security 自动化能力的场景。
2. 方案 B:回调前端页面
- 路径:微信服务器 -> 前端路由 -> 后端接口 (Axios 发送)。
- 前端角色:作为“中转调度员”。
- 实现逻辑:
redirect_uri指向前端路由(如/#/auth-callback)。- 前端从 URL 截取
code,再通过 API 发送给后端。 - 后端仅作为一个普通接口接收
code并换取用户信息。
- 适用场景:需要前端完全控制登录动画、或已有成熟的移动端/多端登录逻辑。
四、 安全参数 state 的管理与校验
state 是防止攻击的关键。其管理逻辑随方案不同而变化。
1. 在“直接回调后端 (方案 A)”中:
- 获取:Spring Security 自动生成并存入 Session。
- 校验:回调时,框架自动比对 URL 中的
state与 Session 中的值。 - 注意:在前后端分离架构下,需解决 Session/Cookie 跨域丢失 问题(配置 CORS 允许 Credentials)。
2. 在“回调前端页面 (方案 B)”中:
- 获取:前端手动生成(随机串)或请求后端生成。
- 存储:存入
localStorage(前端校验) 或 Redis (后端校验)。 - 校验:后端收到前端传来的
code和state后,去 Redis 比对。
五、 多平台登录扩展 (策略模式)
当需要支持微信、GitHub、Google 等多个三方登录时,建议在方案 A 的 successHandler 中采用策略模式:
-
识别平台:通过
registrationId区分来源。 -
分发策略:定义
OAuth2UserHandler接口,为不同平台编写extractUserId实现类。 -
统一处理:最终统一生成系统的 JWT 并重定向回前端。
六、 综合工程代码实现 (四种组合方案)
1. 后端:Spring Boot 核心逻辑
Java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
/**
* 【功能 1】:为“跳转式”生成授权 URL (适配方案 A & B)
*
* @param scheme
* @return
*/
@GetMapping("/wechat-url")
public Map<String, String> getWechatUrl(@RequestParam String scheme) {
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect";
// 方案 A 指向后端,方案 B 指向前端
String redirectUri = "A".equals(scheme) ? "http://api.demo.com/login/oauth2/code/wechat" : "http://www.demo.com/#/auth-callback";
String state = UUID.randomUUID().toString().substring(0, 8);
// 注意:生产环境此处需将 state 存入 Redis
String url = String.format("%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect", baseUrl, "YOUR_APPID", URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), state);
return Collections.singletonMap("url", url);
}
/**
* 【功能 2】:方案 B 的手动换码接口
*
* @param payload
* @return
*/
@PostMapping("/wechat/login")
public ResponseEntity<?> handleSchemeB(@RequestBody Map<String, String> payload) {
String code = payload.get("code");
String state = payload.get("state");
// 1. 校验 state (从 Redis 取)
// 2. 调用微信 API 换取 OpenID
// 3. 生成并返回 JWT
return ResponseEntity.ok(Collections.singletonMap("token", "jwt_for_" + code));
}
}
2. 前端:Vue 3 核心逻辑
Login.vue (二维码展示页)
<script setup>
import { onMounted } from 'vue';
import axios from 'axios';
onMounted(() => {
// 组合 1:JS脚本 + 方案A (直接回后端)
new WxLogin({
id: "qr_a",
appid: "YOUR_APPID",
redirect_uri: encodeURIComponent("http://api.demo.com/login/oauth2/code/wechat"),
state: "security_state_a",
// 建议从后端动态获取
style: "black"
});
// 组合 2:JS脚本 + 方案B (回前端中转)
new WxLogin({
id: "qr_b",
appid: "YOUR_APPID",
redirect_uri: encodeURIComponent("http://www.demo.com/#/auth-callback"),
state: "security_state_b",
});
});
// 方式 2:后端构造 URL 跳转方式 (方案 A 或 B)
const jumpToWechat = async (scheme) =>
{
const { data } = await axios.get(/api/auth/wechat-url?scheme=${scheme});
window.location.href = data.url;
};
</script>
<template>
<div class="login-box">
<div id="qr_a"></div>
<div id="qr_b"></div>
<button @click="jumpToWechat('A')">跳转方式A</button>
</div>
</template>
AuthCallback.vue (方案 B 专属中转页)
<script setup>
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
onMounted(async () => {
const { code, state } = route.query;
if (code) {
// 核心逻辑:手动搬运 code 到后端换取真正的登录凭证
const res = await axios.post('/api/auth/wechat/login', { code, state });
localStorage.setItem('token', res.data.token);
router.push('/dashboard');
}
});
</script>
七、 常见配置陷阱清单
- 域名授权:微信后台配置的“授权回调域”必须与
redirect_uri的域名严格一致。 - URL 编码:
redirect_uri作为参数传递时,必须进行encodeURIComponent处理。 - HTTPS 限制:部分微信接口要求回调地址必须为 HTTPS。
- SameSite 属性:现代浏览器对跨域 Cookie 限制严格,方案 A 需关注
Set-Cookie: SameSite=None; Secure。

浙公网安备 33010602011771号