news 2026/11/2 6:52:28

微信扫码登录实战:Spring Boot + Vue实现OAuth2.0授权码流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信扫码登录实战:Spring Boot + Vue实现OAuth2.0授权码流程

微信扫码注册登录现在几乎是网站应用的标配能力,尤其做面向普通用户的 PC 端产品、To B 管理后台或者独立部署的业务系统,扫码登录都能直接砍掉一截账号体系成本。用户不用记密码,站方也不用发验证码,一套 OAuth2.0 授权码流程就能把身份验证这件事转交给微信。今天这篇不写空话,就基于 Spring Boot + Vue 这套常见技术栈,把微信开放平台网站应用的扫码注册登录从账号申请、参数配置、后端换 token、前端扫码回调,一直写到上线前的常见坑和安全加固。你要是正准备接手登录模块,或者刚被分配了一个“做个微信扫码登录”的需求,可以直接拿这篇当落地清单用。

1. 扫码登录背后的思路拆解:为什么是 OAuth2.0 授权码模式

1.1 我要解决的业务场景到底是什么

先说场景。最常见的需求是这样的:一个网站应用需要支持用户用微信扫码完成注册和登录。用户首次扫码时自动注册,之后每次扫码直接登录,站方后台能看到微信昵称、头像这些基础信息,后续可以再绑定手机号、补齐邮箱。这套东西放在 PC 网站、SaaS 系统、运维后台、企业内部系统上都成立。

这个需求里最大的难点不在于“弹个二维码”本身,而在于三个问题:怎么安全地拿到用户微信身份标识,怎么把微信身份和站内用户绑定,怎么让前端拿到登录态。微信开放平台的网站应用扫码登录,本质上是一套标准 OAuth2.0 授权码流程,我先把这一句话讲清楚,后面所有代码都围绕这句话展开。

1.2 为什么一定选授权码模式,而不是把密钥交给前端

要理解为什么用授权码模式,你得先知道微信为什么不能让你直接把用户信息暴露给一个“不可信”的前端页面。网站的 JS 代码是公开的,任何人都能打开 DevTools 看到请求参数,如果把 AppSecret 这种凭证放前端,等于把家里钥匙贴在大门上。

OAuth2.0 授权码模式的做法是:前端只负责把用户引导到微信的授权页,微信在用户确认之后,通过服务器端重定向给后端一个一次性的code,由后端拿着code去换access_token,再用access_token去拉用户信息。整个过程里,真正值钱的密钥(AppSecret)只存在于后端服务器和微信服务器之间的链路中。你把这个事理解成酒店前台流程:用户拿一张一次性餐券去前台,前台验证餐券后给用户一套带权限的门卡,而不是直接把总卡密码写在取号机上。

1.3 一次扫码背后实际流转了哪些环节

把整个时序过一遍,你在写代码前心里就有谱了:

  1. 用户打开网站登录页,点击“微信扫码登录”。
  2. 前端调用后端接口,后端生成一个随机state并缓存到 Redis,同时拼接微信授权链接返回给前端。
  3. 前端用弹窗或 iframe 打开这个微信授权链接。
  4. 用户用手机微信扫码,在手机上确认授权。
  5. 微信前端将页面重定向(302)到后端配置好的回调地址,地址上带code和state。
  6. 后端先校验state,防止伪造授权请求。
  7. 后端用code调用微信接口换取access_token和openid。
  8. 后端再用access_token拉取微信用户信息,拿到昵称、头像。
  9. 后端用openid查本地用户表,不存在则自动注册。
  10. 后端签发自己的登录凭证(比如 JWT),把前端重定向到登录成功页并携带 token。
  11. 前端保存 token,刷新登录状态。

这里code是一次性的,有效期只有五分钟,用过一次就作废,这也是授权码模式的安全基础。后面章节里的所有代码,都是把这一条链路逐段落地。

2. 微信开放平台账号申请与参数配置,这一步错了后面全白搭

2.1 网站应用创建流程,个人开发者最容易卡住

微信扫码登录不是拿公众号的 AppID 就能接的,必须走微信开放平台(open.weixin.qq.com),而且要创建“网站应用”。创建流程大概是:

  • 注册开放平台开发者账号,完成开发者资质认证。
  • 在“管理中心”里创建“网站应用”。
  • 填写应用名称、官网地址、应用简介。
  • 配置授权回调域。
  • 提交审核,一般一两个工作日完成,审核通过后拿到 AppID 和 AppSecret。

这里最要命的是主体资质。个人开发者经常卡在这一步,因为网站应用目前主要面向企业或者组织类主体,个人主体开通限制很多,申请时要有企业相关的执照或认证材料。如果你只是个人项目自己测试,可以把思路转向微信测试号或者本地 mock,真要做生产环境,流程上绕不开企业资质。

注意:AppID 是公开的,可以出现在前端;AppSecret 是机密的,绝不能打包进前端代码或者提交到 Git。我见过有人为了省事把 AppSecret 直接写在 Vue 的.env文件里,上线当天晚上账号就被刷爆了。

2.2 回调域名和 redirect_uri 之间的关系

开放平台里配置的是“授权回调域”,比如auth.example.com。你在代码里拼接redirect_uri参数时,这个参数的域名必须和配置的授权回调域完全一致,否则微信直接拒绝回调。

这里还有个很隐蔽的坑:redirect_uri参数必须做 URL 编码后再放进授权链接。很多新手拼接 URL 时不编码,或者编码后又被框架解了一层,回调时域名匹配不上,页面一直卡在白屏或者提示“redirect_uri 参数错误”。我在项目里常写的标准配置如下:

配置项示例值常见错误
授权回调域auth.example.com写成了https://auth.example.com/callback
redirect_urihttps://auth.example.com/api/auth/wechat/callback没做 URLEncode
scopesnsapi_login写成了公众号网页授权的snsapi_userinfo

2.3 审核和上线前容易踩的三个小细节

创建应用审核时,一般要求应用有实际可访问的官网主页,并且网页内容里有清晰的应用介绍。我见过审核被拒的几个典型原因:官网打不开、页面是空模板、应用名称和网站内容对不上。提交前自己先拿无痕浏览器验证一遍重要页面能不能正常访问。

另外,授权回调域配置之后不要频繁改。每次修改虽然不需要重新审核,但如果正在线上跑流量,老回调链接会出现短暂不匹配问题。最好先在测试域名上把所有联调做完,再把生产环境的域名一致替换。

3. Spring Boot 后端实现:令牌换取、用户绑定与会话签发

3.1 用户表设计,openid 才是真正的身份锚点

先想清楚库表结构再写接口。微信扫码登录对接的用户表,最关键的是要有openid字段。openid是用户在某个应用(同一个 AppID)下的唯一标识,同一个微信用户在不同 AppID 下的openid不一样。这个字段必须建唯一索引,防止并发扫码导致重复创建用户。

我这里给一个常见的精简设计:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信开放平台下的唯一标识', `nickname` varchar(64) DEFAULT NULL, `avatar` varchar(512) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `last_login_time` datetime DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

如果你接入了多个微信产品(公众号、小程序、网站应用)并且做了同主体绑定,理论上可以拿到unionid去做账号打通。但在只做网站扫码登录的场景下,openid已经完全够用。不要为了以后可能存在的多端打通去过度设计,先满足当前需求,留一个unionid字段备用即可。

3.2 生成扫码链接的后端接口,state 要进 Redis

后端先提供一个接口,比如GET /auth/wechat/qr-url,前端点开弹窗时调用。这个接口的核心动作是:生成随机state,把state存 Redis 并设置 120 秒过期,拼接微信授权 URL。

示例代码如下:

@RestController @RequestMapping("/auth/wechat") public class WechatLoginController { @Value("${wechat.appid}") private String appid; @Value("${wechat.redirect-uri}") private String redirectUri; @Resource private StringRedisTemplate stringRedisTemplate; @GetMapping("/qr-url") public Result<QrUrlVO> getQrUrl() { String state = UUID.randomUUID().toString().replace("-", ""); stringRedisTemplate.opsForValue().set("wechat:state:" + state, "pending", 120, TimeUnit.SECONDS); String url = "https://open.weixin.qq.com/connect/qrconnect" + "?appid=" + appid + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) + "&response_type=code" + "&scope=snsapi_login" + "&state=" + state + "#wechat_redirect"; return Result.ok(new QrUrlVO(url, state)); } }

为什么state要进 Redis?因为回调接口收到state后必须核对它是不是我们下发出去的。如果不校验,攻击者可以构造一个假回调引导用户绑定到恶意账号,这就是典型的登录 CSRF。Redis 里把state和时间绑定,过期值设为 120 秒,用户扫码慢一点可能就过期了,但这是安全性和用户体验的合理折中,过期后提示用户重新生成二维码即可。

3.3 回调接口:校验 state、用 code 换 access_token

微信在用户扫码确认后,会把浏览器重定向到redirect_uri?code=xxx&state=xxx。后端在这个回调里按顺序做四件事:

  • 校验state是否在 Redis 中存在且匹配。
  • 用code请求微信的sns/oauth2/access_token接口。
  • 用返回的access_token和openid去sns/userinfo拉用户信息。
  • 根据openid查本地用户,做登录或注册。

第二步请求微信接口时,推荐用 RestTemplate 或者 OkHttp,代码大概这样:

@GetMapping("/callback") public void callback(@RequestParam("code") String code, @RequestParam("state") String state, HttpServletResponse response) throws IOException { // 1. 校验 state String cached = stringRedisTemplate.opsForValue().get("wechat:state:" + state); if (StringUtils.isBlank(cached)) { response.sendRedirect(frontendUrl + "/login?error=state_invalid"); return; } // 用过的 state 立即删除,防止二次使用 stringRedisTemplate.delete("wechat:state:" + state); // 2. 换取 access_token String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=" + appid + "&secret=" + secret + "&code=" + code + "&grant_type=authorization_code"; String tokenResp = restTemplate.getForObject(tokenUrl, String.class); JsonNode tokenJson = objectMapper.readTree(tokenResp); if (tokenJson.has("errcode")) { response.sendRedirect(frontendUrl + "/login?error=wechat_error&detail=" + tokenJson.get("errmsg").asText()); return; } String accessToken = tokenJson.get("access_token").asText(); String openid = tokenJson.get("openid").asText(); // 3. 拉取用户信息 String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=" + accessToken + "&openid=" + openid; String userResp = restTemplate.getForObject(userInfoUrl, String.class); JsonNode userJson = objectMapper.readTree(userResp); // 4. 处理本地用户 User user = userService.findOrCreate(openid, userJson); String jwt = jwtUtils.generateToken(user.getId()); // 前端重定向时带上 token response.sendRedirect(frontendUrl + "/login-callback?token=" + jwt + "&state=" + state); }

这里有一点必须强调:code换access_token的请求一定要放在后端,不能放在前端。appsecret请求参数如果暴露在浏览器里,攻击者就能冒充应用去换取任意微信用户的身份,这属于事故级漏洞。

3.4 注册绑定逻辑,自动建号的细节坑

userService.findOrCreate里其实藏着很多新手会踩的坑。微信返回的nickname是用户自己的名字,可能带表情符号、可能重复、可能很长。如果你把nickname设成数据库唯一字段,两个微信用户都叫“张三”就会直接炸掉。我的建议是:用户名允许重复,唯一约束只放在openid上。如果业务上必须要求站内用户名唯一,就在注册时拼上随机后缀,比如“张三_8f3k”。

另外,每次扫码登录时最好都从微信拉一次最新的nickname和headimgurl并更新到本地。因为用户在微信里换头像后,如果本地一直存旧头像,两周后用户就会觉得你的系统“数据不同步”。当然,拉用户信息接口有频率限制,不要每次页面刷新都去拉微信,只在扫码登录成功这个时机更新一次。

JWT 签发时,payload 里放用户 ID、签发时间、过期时间就够了,不要放openid和access_token。JWT 是要放在浏览器 localStorage 里的,存越多的敏感信息,泄露面越大。我用 JWT 的过期时间一般设成 7 天,后台管理系统视安全要求缩到 2 小时。

4. Vue 前端实现:扫码弹窗、登录回调与登录态保存

4.1 弹窗扫码方案,iframe 和独立窗口怎么选

前端有两个方案可以把用户送到微信授权页:iframe 内嵌二维码,或者window.open打开独立窗口。

iframe 的优点是不脱离当前页面,用户视觉上“没有离开网站”。微信的qrconnect页面本身允许被 iframe 嵌套,所以在大多数浏览器环境里都能正常显示。但 iframe 有一个实际麻烦:用户扫码确认后,微信页面会把自己重定向到后端回调,后端再重定向到前端登录回调页。如果这一切都发生在 iframe 里,顶层页面并不知道 iframe 内部已经跳转完成,你拿不到跨域 iframe 的状态。

独立新窗口方案更稳。点击登录按钮后window.open打开微信授权地址,主页面继续停留,同时前端以轮询方式向后端查询扫码结果。用户看完授权页后手动关掉窗口,或者前端检测到成功后自动关窗。这个方案在几十个真实项目里跑下来,基本没有浏览器兼容性问题。

4.2 弹窗组件和轮询逻辑,这样写最清晰

配套的还有一个底层数据库访问代码,选型需要自行控制。先看前端页面,点击微信登录按钮时,调用后端接口拿授权链接:

const openWechatLogin = async () => { const { data } = await axios.get('/auth/wechat/qr-url') const qrUrl = data.data.url const state = data.data.state // 打开微信授权窗口 window.open(qrUrl, 'wechatLogin', 'width=700,height=600') // 启动轮询 startPolling(state) }

轮询接口就是后端额外加的一个查询状态接口,比如GET /auth/wechat/status?state=xxx。后端在用户被重定向回callback并且成功签发 JWT 后,把 JWT 临时存到 Redis,key 为wechat:login:success:{state},再让前端轮询取走。

let timer = null const startPolling = (state) => { timer = setInterval(async () => { const { data } = await axios.get('/auth/wechat/status', { params: { state } }) if (data.data && data.data.token) { clearInterval(timer) localStorage.setItem('token', data.data.token) location.href = '/' } }, 1500) // 90秒没扫,停掉轮询 setTimeout(() => clearInterval(timer), 90000) }

轮询间隔 1.5 秒比较平衡。间隔太短会对后端和 Redis 造成额外压力,太长用户扫码后会有明显等待感。临时 JWT 在 Redis 里的过期时间设 5 分钟,基本等于用户扫码的完成时限。

4.3 登录成功后的状态管理

Vue 这边我建议把 token 存在 localStorage,同时在状态管理库(Pinia 或 Vuex)里维护一个isLogin状态。路由守卫里每次跳转前读取 token 并解析是否过期,这部分网上资料很多,不展开讲。

要特别提醒的是,扫码登录后页面跳转携带 token 的形式,在真实部署里要注意 URL 长度不会超限,因为 JWT 可能比较长。更好的做法是后端回调时只返回一个一次性login_code,前端再拿着login_code去后端换正式 JWT。不过对常规内部系统来说,直接在重定向 URL 带 JWT 也能接受,但日志里记得别把 URL 整条打印,否则 JWT 就泄露了。

5. 高频故障排查与上线安全加固,这里都是能直接抄的实操经验

5.1 扫码登录高频疑难杂症排查表

我把自己踩过的和身边同事踩过的问题整理成一张表,基本覆盖了 95% 以上的接入问题:

现象可能原因处理办法
二维码一直转圈加载不出来授权回调域没配置,或 redirect_uri 没编码核对开放平台上回调域,检查 redirect_uri 前后端拼装后是否被转码
回调后提示 state 无效state 过期或缓存 key 拼写不一致确认 Redis 过期时间,检查 state 前后是否加了多余空格
用 code 换 token 报 40029code 只能使用一次,可能回调接口被重复调用在回调接口做幂等,借助日志确认请求次数
拉取用户信息报 48001scope 不是 snsapi_login,或接口权限未开通确认 scope 参数,确认网站应用审核已完成
内嵌 iframe 时微信页面空白部分浏览器禁止第三方 Cookie 或嵌套受限改用独立窗口打开授权链接,或直接整页跳转
页面提示无法从此网站添加应用扩展或用户脚本浏览器安全设置或扩展拦截了登录页脚本换无痕模式或换个浏览器测试,同时检查页面 CSP 是否限制了外部脚本

这里有一条通用排查经验:微信接口返回的errcode和errmsg一定要打印到日志。限制微信接口的正常返回也会给你返回errcode,别把errcode当成正常 JSON 字段忽略掉,它往往是定位问题最快的线索。

5.2 安全加固,这几条上线前必须检查

第一,AppSecret 绝不能写进代码仓库。很多团队喜欢把配置写在application.yml里然后整个项目传到 Git,这等于公开了密钥。正确做法是放到环境变量、配置中心或者密钥管理服务里,并且定期轮换。第二,回调接口要用state做双重校验,而且用完立即删除,防止重放攻击。第三,JWT 的签名密钥要足够长,32 个字节以上,不能是 123456 这种弱密钥。第四,日志脱敏。code、access_token、openid这些字段在日志里打码,或者只打印后半段。第五,用户信息接口的调用频率要限制,微信对sns/userinfo有频率控制,突发流量下容易限流,可以在本地 Redis 里对同一个openid加一分钟缓存。

还有一点不太有人提:扫码登录页本身的接口也要做限流。否则有人写脚本高频请求qr-url,每次都往 Redis 里塞state,能把 Redis 内存打爆。我在生产里给这个接口做过一个简单的每秒并发限制,成本很低但效果很明显。

5.3 登录模块上线自检清单

登录是系统的门,上线前最好对着清单逐项过一遍:

  • 开放平台授权回调域与生产环境 redirect_uri 完全一致
  • state入库且有过期时间,回调时校验并删除
  • AppSecret从环境变量读取,不落 Git
  • 前端 token 存储在安全位置,不放进 URL 以外的日志
  • 回调、轮询接口有基本限流
  • openid字段建了唯一索引
  • 日志中检索不到明文access_token和code
  • 微信返回的非 0 错误码都有日志记录和前端提示

我接过的项目里,凡是上线后出问题的,八成都是前三条没做到位,尤其是回调域名不一致和 state 校验缺失。这两个问题排查起来特别费劲,因为前端看到的永远只是“登录失败”四个大字,用户压根不会去翻网络请求里的重定向地址。

微信扫码登录本身不复杂,复杂度主要藏在配置细节和流程安全性里。把这套流程完整走一遍,你收获的不只是“会调接口”,而是对 OAuth2.0 授权码模式和前后端会话管理有了一个真实的工程化认知。以后接 GitHub、Google、飞书登录,思路完全一样,只是换了个授权服务器和参数名。真到那时候你会感谢当初把 state 校验和 Redis 过期时间都认真写了的自己。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 3:23:52

Dify工程化落地:从本地部署到生产级AI应用闭环

简介&#xff1a;本资源是一份面向AI应用开发者的Dify平台全流程实践指南&#xff0c;专为具备编程基础、希望快速构建LLM应用的开发者与技术爱好者设计&#xff0c;解决从零部署到高阶定制的全链路问题。文档覆盖智能客服、内容生成、数据分析等典型场景&#xff0c;系统讲解可…

作者头像 李华
网站建设 2026/11/2 6:52:05

Win11离线安装.NET Framework 3.5实战指南

简介&#xff1a;本资源是一份面向Windows系统运维人员、企业IT支持工程师及离线环境部署人员的实用技术文档&#xff0c;专门解决Win11离线状态下无法启用.NET Framework 3.5这一高频兼容性难题。文档基于微软官方DISM部署方案&#xff0c;完整梳理了从下载匹配ISO镜像、挂载获…

作者头像 李华
网站建设 2026/10/25 21:05:14

客户唤醒自动化实践:定时任务+企微API触达链路全解析

做客户运营的朋友应该都有同感&#xff1a;拉新那一下大家都舍得花钱&#xff0c;但拉来之后呢&#xff1f;名单躺在 CRM 里&#xff0c;超过三十天没有互动&#xff0c;从“高意向客户”悄悄变成了“沉默客户”。销售靠手机通讯录一个个手动跟进&#xff0c;一天撑死一百多条&…

作者头像 李华
网站建设 2026/11/2 6:52:05

自动化与脚本实战指南:从测试框架到系统脚本排错

自动化与脚本这两个词&#xff0c;放在一起看可能觉得稀松平常&#xff0c;但真正深入进去之后你会发现&#xff0c;它几乎覆盖了技术圈里所有“让机器替人干活”的玩法。从测试工程师每天跑的pytest用例&#xff0c;到运维手写的shell脚本&#xff0c;再到办公族用影刀拖拽出来…

作者头像 李华
网站建设 2026/11/2 6:51:02

进程图像与收缩:彻底搞懂进程管理的核心机制

我最近在复习操作系统里的进程管理&#xff0c;越看越觉得“进程图像”“收缩”这两个词被大家一笔带过的次数太多了。考试考到“进程切换时做了什么”&#xff0c;很多人只会答“保存现场、恢复现场”&#xff0c;但现场是什么、保存在哪、什么时候“收缩”什么时候“膨胀”&a…

作者头像 李华
网站建设 2026/11/1 9:50:26

零基础学GPU内核驱动:自旋锁、信号量与工作队列的并发实践

对大部分刚接触内核态编程的人而言&#xff0c;尤其是像我一样盯着 GPU 驱动源码发过呆的人&#xff0c;一上来拦住去路的往往不是那些晦涩的寄存器&#xff0c;而是另外一件事&#xff1a;自旋锁、信号量、工作队列这些同步原语到底怎么用、什么时候用哪一把。GPU KMD&#xf…

作者头像 李华