微信扫码登录从原理到实战:OAuth2.0授权与Spring Boot实现 📅 发布时间:2026/9/9 21:54:43 👁 浏览次数: 当产品经理或老板抛出一句“就加个微信扫码登录而已”的时候很多人会觉得这只是一个二维码的事。用户在登录页拿起手机扫一下点击确认页面跳转整个过程看起来确实不超过十秒。但真正接过的开发者知道这句轻飘飘的需求背后是一条完整的授权链路开放平台应用、回调域名校验、OAuth 2.0 授权码交换、openid 与 unionid 的账号映射、登录态签发以及一串容易在联调阶段反复出现的怪问题。这篇文章想把这件事讲透微信扫码登录为什么不是“加个二维码”它到底要经过哪些环节真正落地时该怎么设计后端接口和前端页面以及那些常见的坑分别怎么排查。无论你是后端开发、前端开发还是被需求方反复催促的团队技术负责人这篇文章都可以当作一份可以直接参考的接入手册。读完你会得到一套基于 Spring Boot 的微信扫码登录最小实现包含二维码生成、回调处理、授权码换 token、用户信息获取、前端轮询登录状态同时能搞清楚微信开放平台和微信公众号网页授权之间的区别避免在最基础的地方选错方案。1. 为什么“加个微信扫码登录”听起来轻巧做起来却是多条链路先说结论用户看到的二维码是整条流程的最后一步而开发要解决的是二维码出现之前和扫码之后的所有问题。从产品视角看需求是“用户在 PC 端登录页扫码登录”。从技术视角看这个需求天然被拆成了四段第一段资质准备。微信扫码登录不是随便拿一个公众号就能做的它依赖微信开放平台里的“网站应用”。开发者需要注册开放平台账号创建网站应用填写应用信息等待审核。正式开发的先决条件并不是写代码而是这些账号和应用配置。第二段OAuth 2.0 授权流程。扫码登录本质上是让用户授权你的网站获取他的微信身份。微信的授权流程会经历“生成授权链接 - 用户扫码确认 - 微信跳转回调地址并携带 code - 后端用 code 换取 access_token - 获取用户信息”这几步。每一步都可能因为参数、域名、code 有效期等问题失败。第三段账号体系打通。微信返回给你的不是一个手机号而是 openid 或 unionid。你需要在本地用户表里找到或创建对应用户决定“首次扫码直接注册”还是“引导绑定已有账号”然后把微信身份和你的业务账号关联起来最后再签发你自己的登录态。第四段前后端协同。扫码发生在手机微信里但登录页在 PC 浏览器上。用户确认登录后微信会把授权结果回调到后端但 PC 端怎么知道“刚才那个二维码已经登录成功了”这里通常要设计轮询、WebSocket 或 SSE让 PC 端能感知到登录结果。所以“加个微信扫码登录”真正消耗的工作量在二维码之外。把这四条链路理清楚就不会再把这项需求当成简单插件的搬运工作。这篇文章后面的内容就是围绕这四段链路逐步展开。2. 微信扫码登录的核心概念与授权原理要理解扫码登录先要理解 OAuth 2.0 的授权码模式。微信扫码登录使用的正是授权码模式只是它把“用户输入账号密码授权”换成了“扫码并确认”。2.1 授权码模式的五个参与者一个典型的微信扫码登录场景里有五个角色角色微信扫码登录里的对应物资源所有者扫码的微信用户客户端你的网站或者 Web 应用授权服务器微信开放平台的授权服务资源服务器微信的 openid、用户信息接口回调地址 redirect_uri你的后端接口接收授权码用户扫码并确认后微信授权服务器不会直接把用户信息交给网站而是跳转到你的后端回调地址附带一个一次性授权码 code。网站后端拿这个 code 去换 access_token再用 access_token 请求用户信息接口。2.2 扫码之后到底发生了什么按时间顺序一次完整的扫码登录包含以下步骤PC 浏览器打开登录页向你的后端请求一个登录二维码。后端生成一个微信 OAuth 2.0 授权链接链接内容类似https://open.weixin.qq.com/connect/qrconnect?appid...redirect_uri...scopesnsapi_loginstate...。前端把这个授权链接渲染成二维码图片。用户用手机微信扫一扫微信先验证链接里的 appid 是否属于合法的开放平台应用。用户在手机上点击“确认登录”。微信浏览器跳转到授权链接中的 redirect_uri并自动拼接code和state参数。你的后端接口收到 code先校验 state再调用微信接口换取 access_token。拿着 access_token 和 openid 调用微信用户信息接口拿到昵称、头像、unionid 等资料。在本地用户表里查找或创建账号签发自己的登录态。PC 端通过轮询等方式感知登录成功跳转首页。很多人会误以为二维码里存的是“用户信息”或“登录凭证”其实二维码内容只是一个授权跳转地址。它本身不含任何账号信息也不能直接用来登录真正的登录凭证是在授权流程完成后由微信临时签发的 code再由后端换取的 access_token。理解这一点后面排查问题会轻松很多。2.3 容易被绕晕的几个术语接入微信登录时最常出现的术语是 appid、appsecret、openid、unionid、access_token、code、state。它们分别解决不同层面的问题appid / appsecret你的应用的身份证和密钥。微信靠 appid 识别是哪个应用在请求授权靠 appsecret 校验换取 token 的请求是否合法。appsecret 只能放在后端绝不能出现在前端代码里。code授权码模式中的临时票据有效期很短通常只有几分钟并且只能使用一次。重复使用会报错。access_token换取用户信息的凭证默认有效期为 7200 秒。它和“全局 access_token”是两回事微信小程序、公众号、开放平台各自的 token 不能混用。openid用户在某个应用下的唯一标识。同一个用户在你的网站应用下和你的公众号下openid 是不同的。unionid同一微信开放平台账号下的多个应用之间用户唯一身份标识。只有绑定了开放平台账号的公众号、小程序、网站应用才能通过 unionid 识别出“这是同一个人”。state客户端生成的一个随机字符串用来防止 CSRF 攻击同时用于把扫码状态和登录请求关联起来。在这些概念里最重要但也最容易混淆的是 openid 和 unionid。如果只在单一网站应用里做用户管理openid 已经够用。如果业务同时有公众号、小程序、PC 网站希望识别出“同一微信用户在多个端的同一身份”就必须依赖 unionid。3. 接入前的准备开放平台账号、网站应用与回调域名写代码之前先把账号和应用准备好。很多第一次接入的团队在这里就会卡住原因是误以为“微信扫码登录”和“微信公众号网页授权”是一回事。3.1 微信开放平台与公众号的区别微信公众号网页授权适用于在微信内置浏览器里打开的 H5 页面需要的是公众号scope 通常是snsapi_userinfo或snsapi_base。它解决的是“用户在微信里打开页面时免登录”的问题。微信开放平台网站应用扫码登录适用于 PC 浏览器上的网站需要在微信开放平台注册成为开发者并创建“网站应用”scope 固定为snsapi_login。用户用微信扫 PC 页面的二维码完成登录。如果你在 PC 网站上硬套公众号网页授权最终走不通。反过来如果在手机微信内的 H5 里放一个 PC 扫码登录二维码体验也非常奇怪。先明确使用场景再决定申请哪一种能力。3.2 需要准备的物料进入微信开放平台后按后台指引创建“网站应用”通常需要准备网站应用的名称、简介、图标。网站首页地址。授权回调域名。这个域名必须与授权链接里的redirect_uri保持一致通常要求是已完成备案的公网域名并使用 HTTPS。开放平台账号的开发者认证信息。这里要特别强调回调域名的重要性。微信扫码登录的回调地址不是随便写的它必须是你在开放平台后台配置过的域名。很多联调问题例如“扫码后提示 redirect_uri 参数错误”第一排查对象就是后台配置与实际 redirect_uri 不一致。3.3 本地开发环境怎么处理回调微信服务器要访问你的回调接口意味着你的本地服务不能只监听localhost。开发阶段常用的办法是使用内网穿透工具比如 cpolar、Natapp、花生壳等把本地的 8080 端口映射成一个临时公网 HTTPS 域名。本地调试时的回调域名可以先用内网穿透工具生成的域名但在微信开放平台后台配置时需要准确配置这个临时域名。等正式环境部署后再把后台配置和代码里的redirect_uri切换成线上域名。无论本地还是线上都建议把redirect_uri做成配置项而不是硬编码在代码里。生产环境https://your-domain.com/api/wechat/callback本地测试https://your-tunnel-domain/api/wechat/callback4. Spring Boot 后端实现生成二维码与回调处理环境假设JDK 8 以上Spring Boot 2.x 或 3.xMaven 构建使用spring-boot-starter-web模拟接口调用。代码中会用到 RestTemplate项目里如果没有可以先引入基础 Web 依赖并创建配置类。4.1 项目依赖与基础配置在pom.xml中加入 Web 依赖和 LombokLombok 只是为了减少 getter/setter 样板代码不习惯可以手写。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency在src/main/resources/application.yml中增加微信相关配置server: port: 8080 spring: application: name: wechat-login-demo wechat: app-id: wx1111111111111111 app-secret: your_app_secret # 这个地址必须和微信开放平台后台配置的回调域名一致 redirect-uri: https://your-domain.com/api/wechat/callback创建配置属性类// 文件路径src/main/java/com/example/wechatlogin/config/WechatProperties.java Data Component ConfigurationProperties(prefix wechat) public class WechatProperties { private String appId; private String appSecret; private String redirectUri; }创建 RestTemplate 配置类// 文件路径src/main/java/com/example/wechatlogin/config/RestTemplateConfig.java Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }4.2 生成微信授权链接微信扫码登录的授权链接格式是固定的。后端生成链接后由前端把这个链接渲染成二维码。// 文件路径src/main/java/com/example/wechatlogin/service/WechatAuthService.java Service public class WechatAuthService { private final WechatProperties wechatProperties; private final RestTemplate restTemplate; public WechatAuthService(WechatProperties wechatProperties, RestTemplate restTemplate) { this.wechatProperties wechatProperties; this.restTemplate restTemplate; } /** * 生成微信扫码登录的授权 URL。 * 前端拿到这个 URL 后转成二维码图片。 */ public String buildQrCodeUrl(String state) { String redirectUri URLEncoder.encode(wechatProperties.getRedirectUri(), StandardCharsets.UTF_8); return https://open.weixin.qq.com/connect/qrconnect ?appid wechatProperties.getAppId() redirect_uri redirectUri response_typecode scopesnsapi_login state state #wechat_redirect; } }这里有几个细节需要注意redirect_uri在拼接前必须做 URL 编码否则微信会解析错误。scope必须固定为snsapi_login这是开放平台网站应用的扫码登录授权方式。state不能省略它既是 CSRF 防护也是后续前端轮询识别此次登录请求的依据。链接末尾的#wechat_redirect是微信要求的固定写法。4.3 二维码状态与登录态管理由于用户扫码后手机微信和 PC 浏览器是两个完全独立的客户端需要一种机制把“微信回调结果”和“PC 端轮询请求”对应起来。最简单的办法是用同一个 state 作为 key将扫码状态和登录结果存放在服务端。下面用一个简化版 LoginService 来演示这个思路// 文件路径src/main/java/com/example/wechatlogin/service/LoginService.java Service public class LoginService { private final MapString, Long pendingStates new ConcurrentHashMap(); private final MapString, String loginResults new ConcurrentHashMap(); /** * 生成并暂存一个 state默认 10 分钟有效。 */ public String createState() { String state UUID.randomUUID().toString().replace(-, ); pendingStates.put(state, System.currentTimeMillis() 10 * 60 * 1000L); return state; } /** * 校验并消费 state。校验通过后立即移除避免重复使用。 */ public boolean checkState(String state) { Long expire pendingStates.remove(state); if (expire null) { return false; } return expire System.currentTimeMillis(); } /** * 保存登录结果供 PC 端轮询获取。 */ public void saveLoginResult(String state, String token) { loginResults.put(state, token); } /** * PC 端轮询时获取登录结果。 */ public String getLoginResult(String state) { return loginResults.get(state); } }生产环境不要用 ConcurrentHashMap 存放这类状态因为服务重启会丢失多实例部署时也会出现“状态在 A 机器、请求却落在 B 机器”的问题。这里只是为了演示流程落地时建议换成 Redis并给 key 设置过期时间。4.4 请求二维码接口与回调接口Controller 层需要三个接口前端获取二维码链接。微信回调接口接收 code 和 state。PC 端轮询登录状态接口。// 文件路径src/main/java/com/example/wechatlogin/controller/LoginController.java RestController RequestMapping(/api/wechat) public class LoginController { private final WechatAuthService wechatAuthService; private final LoginService loginService; public LoginController(WechatAuthService wechatAuthService, LoginService loginService) { this.wechatAuthService wechatAuthService; this.loginService loginService; } GetMapping(/qrcode) public MapString, String qrcode() { String state loginService.createState(); String qrUrl wechatAuthService.buildQrCodeUrl(state); MapString, String result new LinkedHashMap(); result.put(qrUrl, qrUrl); result.put(state, state); return result; } GetMapping(/callback) public String callback(RequestParam(code) String code, RequestParam(state) String state) { if (!loginService.checkState(state)) { return state 校验失败请返回登录页重新扫码; } String token wechatAuthService.loginByWechatCode(code, state); loginService.saveLoginResult(state, token); return 登录成功可以关闭此页面; } GetMapping(/poll) public MapString, Object poll(RequestParam(state) String state) { String token loginService.getLoginResult(state); MapString, Object result new LinkedHashMap(); if (token ! null) { result.put(status, success); result.put(token, token); } else { result.put(status, pending); } return result; } }这里回调接口返回的是一段纯文本或简单 HTML。实际项目中可以返回一个“登录成功”提示页也可以根据业务需要跳转到指定地址。PC 端真正关注的是/poll接口是否能轮询到登录结果。4.5 使用 code 换取 access_token 并获取用户信息loginByWechatCode是后端最核心的一段逻辑。它的任务是用 code 换取 access_token再拿 access_token 和 openid 获取微信用户信息。// 文件路径src/main/java/com/example/wechatlogin/service/WechatAuthService.java public String loginByWechatCode(String code, String state) { // 1. 用 code 换取 access_token String tokenUrl https://api.weixin.qq.com/sns/oauth2/access_token ?appid wechatProperties.getAppId() secret wechatProperties.getAppSecret() code code grant_typeauthorization_code; MapString, Object tokenResult restTemplate.getForObject(tokenUrl, Map.class); if (tokenResult null || tokenResult.get(errcode) ! null) { throw new RuntimeException(微信换取 access_token 失败 tokenResult); } String accessToken String.valueOf(tokenResult.get(access_token)); String openid String.valueOf(tokenResult.get(openid)); // 2. 获取用户信息 String userInfoUrl https://api.weixin.qq.com/sns/userinfo ?access_token accessToken openid openid langzh_CN; MapString, Object userInfo restTemplate.getForObject(userInfoUrl, Map.class); if (userInfo null || userInfo.get(errcode) ! null) { throw new RuntimeException(获取微信用户信息失败 userInfo); } String nickname String.valueOf(userInfo.getOrDefault(nickname, )); String headimgurl String.valueOf(userInfo.getOrDefault(headimgurl, )); String unionid userInfo.containsKey(unionid) ? String.valueOf(userInfo.get(unionid)) : null; // 3. 找到或创建本地用户 String userId findOrCreateUser(openid, unionid, nickname, headimgurl); // 4. 签发登录态。示例直接生成 UUID生产环境建议签发 JWT 或写入 Redis Session。 String token UUID.randomUUID().toString().replace(-, ); saveUserToken(token, userId); return token; }代码里的findOrCreateUser和saveUserToken是业务相关方法这里不展开完整的数据库实现。核心思路是优先用unionid作为跨应用身份标识无法获取 unionid 时退化为使用openid。查询本地用户表是否已存在该微信身份。存在则直接复用账号不存在则创建新用户或进入绑定流程。签发的登录态是“自己的登录态”不要直接把微信 openid 或 access_token 作为登录凭证返回给前端。如果在生产环境中使用 Map 解析微信接口返回值建议进一步封装为 DTO并用统一异常处理捕获网络异常、错误码和字段缺失。演示代码优先保证链路清晰工程化之后需要加强类型和异常处理。5. 前端页面二维码渲染与轮询登录状态前端页面要完成三件事从后端获取二维码链接、把链接渲染成二维码图片、轮询登录结果。这里以原生 HTML JavaScript 为例二维码生成使用qrcodejs或qrcode这类库。可以先把库下载到本地静态目录也可以直接引用 CDN但生产环境建议下载到自己的静态资源服务器。!-- 文件路径src/main/resources/static/login.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title微信扫码登录/title script src/js/qrcode.min.js/script /head body div stylewidth: 260px; margin: 100px auto; text-align: center; div idqrcode/div p idstatus请使用微信扫码登录/p /div script let state ; let timer null; // 1. 向后端请求二维码链接和 state fetch(/api/wechat/qrcode) .then(res res.json()) .then(data { state data.state; new QRCode(document.getElementById(qrcode), { text: data.qrUrl, width: 240, height: 240 }); startPolling(); }) .catch(() { document.getElementById(status).innerText 二维码加载失败请刷新重试; }); // 2. 轮询登录状态 function startPolling() { timer setInterval(() { fetch(/api/wechat/poll?state state) .then(res res.json()) .then(data { if (data.status success) { clearInterval(timer); document.getElementById(status).innerText 登录成功; // token 可以写入 localStorage也可以交给后续接口换取用户信息 localStorage.setItem(token, data.token); window.location.href /home; } }) .catch(() { // 网络异常时保持轮询但需要避免无限轮询 }); }, 2000); } /script /body /html轮询间隔设置为 2 到 3 秒比较合适太短会对后端造成不必要的压力太长会降低体验。实际项目中还可以加入一个最长等待时间比如 2 分钟后如果还没有登录成功就停止轮询并提示用户重新刷新二维码。这里有一个容易被忽略的体验问题用户在手机微信里确认登录后微信内置浏览器会跳转到回调地址手机端看到的是“登录成功”页面。但 PC 端需要等下一次轮询才会感知到结果所以 PC 端可能比手机端晚 1 到 3 秒跳转。这是扫码登录的正常现象不需要担心。6. 运行结果与效果验证6.1 启动项目在项目根目录执行mvn spring-boot:run启动成功后浏览器访问http://localhost:8080/login.html如果本地服务需要通过内网穿透工具暴露公网地址需要先启动内网穿透服务把本地 8080 端口映射为公网 HTTPS 地址再检查微信开放平台后台配置的回调域名与项目application.yml里的redirect-uri是否一致。6.2 完整登录链路验证按照以下步骤验证打开登录页确认二维码能正常显示。使用手机微信扫一扫。手机微信弹出“确认登录”页面。点击确认。手机微信内置浏览器跳转到回调地址显示“登录成功可以关闭此页面”。PC 端页面在几秒内轮询到登录结果自动跳转或显示 token。如果以上步骤都能完成说明从二维码生成到回调再到 token 交换和前端轮询的整条链路已经打通。6.3 失败时从哪里查起这条链路涉及 PC 浏览器、手机微信、微信服务器、后端服务四个环节失败时建议按顺序排查第一个接口直接访问/api/wechat/qrcode确认后端是否返回qrUrl和state。检查授权链接里的redirect_uri是否经过 URL 编码是否与开放平台后台配置一致。用手机扫码后确认手机端的回调地址是否真的被微信跳转到了。查看后端日志重点关注换取access_token的接口返回了什么错误码。确认前端轮询的state与后端生成二维码时返回的state是同一个。7. 常见问题与排查思路下表列出微信扫码登录接入过程中最常遇到的几类问题。这些问题大多不是代码逻辑复杂而是参数、有效期、域名配置等环节不一致导致的。问题现象可能原因排查方式解决方案扫码后提示“redirect_uri 参数错误”回调域名在微信开放平台后台未配置或实际 redirect_uri 与后台配置不一致对比后台配置和代码中的 redirect_uri检查是否经过 URL 编码在开放平台后台配置正确的回调域名保持与代码一致扫码后提示应用未通过审核或参数错误网站应用仍在审核中或 appid 不属于当前开放平台账号登录开放平台查看应用状态是否正确等待审核完成或检查 appid 是否复制错误后端日志返回 40029 invalid codecode 已过期或 code 被重复使用检查后端是否因为接口重试导致同一 code 被提交两次确保 code 只用一次避免回调接口被重复触发返回 40001 access_token 无效使用了全局 access_token 或公众号 access_token 调用 sns/userinfo打印日志确认 used 的是网页授权 access_token使用/sns/oauth2/access_token返回的 access_token返回 48001 或 40003 用户信息获取失败openid 与 access_token 不匹配或授权作用域不足核对 openid 是否来自同一个 access_token 换取结果同时使用同一接口返回的 access_token 和 openidPC 端长时间没有登录成功前端轮询 state 与后端保存状态不一致或服务端重启导致状态丢失查看前端轮询请求的 state 参数对比后端日志统一 state 生成与传递逻辑生产环境改用 Redis本地回调接不到微信跳转本地服务没有公网地址检查内网穿透工具是否正常开放平台配置的回调域名是否是临时公网域名使用内网穿透工具暴露 HTTPS 地址重新配置回调域名二维码能显示但微信扫码后没有任何反应授权链接中的 scope 不是 snsapi_login或被微信拦截访问授权链接看是否能打开授权页面将 scope 设置为 snsapi_login并确认链接完整微信开放平台的错误码文档会根据业务场景更新遇到代码里没有覆盖的错误码时优先去官方文档检索对应错误码含义。上面几种是接入过程中出现频率最高的。8. 最佳实践与工程建议一个能用的扫码登录功能和能安全稳定上线的扫码登录功能差的往往是下面这些细节。8.1 state 必须校验并且只能使用一次state 参数的第一作用是防止 CSRF。攻击者可以把一个合法授权链接塞给受害者扫码如果后端不校验 state就可能导致受害者账号被绑定到攻击者指定的状态中。正确的做法是生成二维码时把 state 保存到服务端回调时校验并立即删除拒绝重复使用。8.2 code 和 access_token 的安全处理code 是一次性凭证不能写入日志不能缓存复用。access_token 代表用户在本次授权中的访问能力原则上也不能明文落库。如果业务必须保存应该加密存储或保存在专门的凭证服务中并设置过期处理任务。前端拿到的只是你业务自己签发的登录态不应该把微信的 access_token 传回前端。8.3 区分 openid 和 unionid 的使用场景如果只有一个网站应用用 openid 作为用户唯一标识就够了。如果业务同时有小程序、公众号、PC 网站建议通过开放平台的 unionid 打通所有端的用户身份。在设计用户表时可以预留wechat_openid、wechat_unionid两个字段或者单独建一张微信绑定表保存用户 ID、appid、openid、unionid 和绑定时间。8.4 首次扫码登录的账号绑定策略首次扫码后是自动注册一个新账号还是要求绑定已有账号这是产品决策不是技术决策。比较稳妥的策略分两层如果业务允许微信身份直接进入首次扫码自动创建账号并登录。如果业务强依赖手机号或实名信息第一次扫码后先进入绑定页用户输入手机号和验证码验证通过后再把微信身份绑定到已有账号之后再次扫码直接登录。绑定已有账号属于敏感操作绑定前一定要做手机号验证码校验防止微信身份被恶意绑定到他人账号。8.5 前端轮询与超时控制轮询一定要加超时控制。建议前端记录一个开始时间比如 2 分钟内未登录成功就停止轮询提示用户刷新二维码继续操作。后端对应的 state 和登录结果也应该设置过期时间避免 Redis 里堆积过期 key。8.6 生产环境状态存储演示代码使用 ConcurrentHashMap 保存 state 和登录结果生产环境必须替换为具备 TTL 能力的存储比如 Redis。多实例部署时后端所有实例都能访问同一个 Redis才能保证 PC 端的轮询请求无论负载均衡到哪台机器都能读到手机端回调写入的登录结果。同时建议把 appid、appsecret、redirect-uri 放到配置中心或环境变量中不要提交到代码仓库。8.7 日志、监控与错误码告警微信扫码登录涉及外部系统联调期和线上都很依赖日志。至少应该在三个位置打日志二维码生成时记录 state 关键信息不要记录完整 redirect_uri 也无妨、回调接收时记录 code 是否存在、换取 access_token 时记录微信返回的错误码。线上环境还可以针对错误码配置告警比如频繁出现 40029 或 40001 时说明凭证或 code 使用逻辑可能出了问题。8.8 安全边界回调接口没有微信签名意味着攻击者也可以直接构造一个 GET 请求访问/api/wechat/callback只是没有有效 code换取 token 会失败。安全的关键仍然在于 code 的不可猜测和一次性以及后端绝不能把成功换取的 token 信息直接暴露在回调页面 URL 中。整个流程建议全程使用 HTTPSappsecret 只在服务端使用通过配置中心管理并定期轮换。9. 最后再聊“为什么不能加个微信扫码登录”下次再被问“为什么就不能加个微信扫码登录”答案不是“不能”而是“可以但有一些前置条件”。你可以直接说需要先确认微信开放平台网站应用是否已经申请好回调域名是否已配置为 HTTPS后端是否预留了开发和联调的时间。只要产品愿意把这些条件准备好扫码登录本身并没有那么神秘。这篇文章真正想传达的判断是微信扫码登录的复杂度不在二维码而在授权链路与账号体系的衔接。工具链可以用 Spring Boot RestTemplate 快速跑通但生产环境要考虑状态存储、安全校验、绑定策略和监控告警。把这部分想清楚扫码登录才不是“应付需求”而是真正可长期维护的登录能力。后续如果想继续深入可以从这几个方向入手把演示代码中的 UUID 登录态替换为 JWT并解决 token 刷新和续期问题将状态存储从内存 Map 迁移到 Redis并对比不同 key 设计对并发的影响研究同一开放平台下公众号、小程序、网站应用如何通过 unionid 统一用户身份了解 OAuth 2.1 和 PKCE 的演进逻辑理解微信授权设计在移动端和 Web 端的差异。先把这套最小实现跑通再一步步补齐工程细节就不会被“加个二维码”的需求打乱了节奏。