做后台系统或者 PC 网站时产品经常提一个需求网页上加一个微信扫码登录。听起来就是放个二维码的事可真做起来从二维码怎么渲染、回调地址怎么接、code 怎么换用户信息到怎么和自家账号体系绑定每一步都有需要注意的地方。我最初接到这个需求时先翻了一通微信开放平台文档看完脑子里只有概念动手一写才发现文档和实际落地之间隔着不少坑。这篇文章把我实际走通的完整链路整理出来适合刚接触 OAuth2.0 或者被分配了扫码登录需求的前后端同学参考。代码示例按前后端分离的思路写前端负责二维码展示和回调页处理后端负责安全校验和与微信服务器交换数据。1. 需求拆解先分清“扫码登录”和“网页授权”1.1 两种“微信登录”不是一回事在我带过的新人里最常犯的错误就是把“微信开放平台扫码登录”和“微信公众平台网页授权”弄混。这两种能力名字听起来差不多但应用场景完全不同开放平台扫码登录用户在 PC 浏览器里访问网页页面上展示一个二维码用户掏出手机微信扫一扫授权后 PC 网页登录成功。这是本篇要做的场景。公众平台网页授权用户用微信内置浏览器打开一个 H5 页面在页面里发起授权授权后直接获得用户身份信息常用于公众号菜单里的页面或者微信内打开的链接。很多同学接扫码登录结果拿的是公众号的 AppID页面怎么写都报 redirect_uri 错误就是因为把这两种体系的凭据混用了。下面用表格区分一下对比项开放平台网站应用扫码登录公众平台网页授权使用场景PC 浏览器中的网页微信内置浏览器打开的 H5凭据来源微信开放平台 - 网站应用微信公众平台 - 网页授权授权方式手机扫码确认页面内点击授权常见 scopesnsapi_loginsnsapi_base 或 snsapi_userinfo获取内容openid、unionid、头像昵称openid、unionid、头像昵称前提条件开放平台账号网站应用审核认证服务号部分能力如果是“网页展示二维码让用户扫码”八成是第一种也就是微信开放平台里的网站应用。1.2 扫码登录不只是一张二维码OAuth2.0 授权码模式二维码只是入口背后走的是标准的 OAuth2.0 授权码模式。整个过程里浏览器是客户端微信授权服务器是负责“确认身份”的第三方你自己的后端是真正去微信拿用户数据的一方。我用大白话解释下这个流程用户扫二维码相当于拿着你的网站开的一张“介绍信”去微信那里申请一张“临时凭证”。微信确认用户身份后把临时凭证也就是 code 交给你的后端。你的后端再拿着这个 code 加上自己手里的密钥去找微信换正式的访问凭证 access_token然后用 access_token 拉取用户信息。具体拆成四步前端页面构造授权链接跳转到微信二维码页或者在自己的页面内渲染二维码。用户用手机微信扫码并确认授权。微信服务器把浏览器重定向到配置的 redirect_uri地址上带着一个授权码 code 和原样带回的 state。后端收到 code 后配合 AppSecret 去微信接口换 access_token拿到 openid 和用户资料再给前端签发自家系统的登录态。这个模式的核心规则是AppSecret 只能出现在后端前端永远不能碰。谁在浏览器里暴露了 AppSecret基本上等同于把自己的账号体系拱手让人。1.3 前后端职责划分与整体流程前后端分离的项目里扫码登录的职责边界非常清晰步骤参与方做的事情1. 进入登录页前端页面展示二维码2. 用户扫码确认手机微信用户确认授权3. 回调后端接口接收 code 和 state4. 换取 access_token后端请求微信接口5. 拉取用户信息后端请求微信接口6. 签发登录态后端绑定/创建用户返回自家 token7. 跳转业务页前端页面保存 token进入系统把 code 换取 token 的步骤放在后端不只是为了安全还有一个实际好处后端可以统一记录日志排查问题时能看到微信到底返回了什么而不是在前端网络面板里一脸懵。2. 接前准备账号资质、回调域与关键参数2.1 注册开发者账号并创建“网站应用”这一步常在文档里一带而过但实际是最容易卡住人的。微信开放平台注册需要企业或组织主体个人开发者想创建“网站应用”通常会被资质审核挡住。如果你只是个人练手建议先找公司资质或者用已认证的组织账号。具体步骤进入微信开放平台注册开发者账号并完成开发者认证。在“管理中心”里选择“网站应用”点击创建应用。填写网站名称、网站首页地址、应用简介等信息提交审核。审核通过后在应用详情页可以看到 AppID 和 AppSecret。拉到“授权回调域”配置区填入接收回调的域名。AppSecret 一般在创建时完整展示一次之后只能重置不会再次提供。我习惯把它直接放到服务端配置中心用环境变量管理不硬编码在代码里。2.2 授权回调域规则、误区和本地调试“授权回调域”是扫码登录参数体系里配置要求最严格的一项。微信后台要求填的是一个域名不包含协议头、不包含路径、不包含端口比如your-site.example.com。然后你在构造 redirect_uri 时才能填https://your-site.example.com/auth/wechat/callback这样的完整地址。这里最容易踩坑的有三个点后台填的是域名不是 URL。填https://your-site.example.com会报错。redirect_uri 的域名必须和后台配置一致包括主域名、子域名都要严格匹配。构造 URL 时redirect_uri 必须要做 URL 编码否则参数解析会出问题。本地开发环境用http://localhost:8080是肯定不行的微信服务器没法回跳到你的本机。我在项目里常用的调试方案是准备一个测试域名通过内网穿透工具把本地服务临时映射到公网然后在开放平台后台把测试域名加入授权回调域。这样本地代码改完扫码后微信能直接回调到本机接口联调效率高很多。生产环境上线前一定要把 redirect_uri 切回正式域名不要把临时隧道地址留到线上否则回调随时可能断掉。2.3 参数速查表每个参数都要知道为什么构造授权链接时下面这些参数缺一不可参数含义示例注意事项appid应用唯一标识wx1234567890abcdef来自开放平台网站应用redirect_uri授权后跳转地址https://your-site.example.com/auth/callback必须 URL 编码response_type固定为 codecodeOAuth2 授权码模式scope授权范围snsapi_login扫码登录固定值state防 CSRF 随机串a1b2c3d4e5每次独立生成回调时校验state的作用容易被忽视。它是一串随机数前端生成后存到本地授权回调时会原样带回。后端通过对比 session 或缓存里的 state能确认这次回调是不是自己发起的授权请求防止攻击者伪造回调地址来钓鱼。我见过有人图省事把 state 写死这是非常危险的做法每次授权都应该生成新的随机值。scope也别乱改。扫码登录就是snsapi_login你要是按公众号网页授权的习惯写成snsapi_userinfo微信会直接拒绝。3. 前端落地两种二维码实现方案与回调处理3.1 方案一直接跳转到官方二维码页最省事的做法是把页面直接重定向到微信官方的二维码页面。代码很简单const appid wx1234567890abcdef; const redirectUri encodeURIComponent(https://your-site.example.com/auth/wechat/callback); const state Math.random().toString(36).slice(2); const scope snsapi_login; const url https://open.weixin.qq.com/connect/qrconnect?appid${appid}redirect_uri${redirectUri}response_typecodescope${scope}state${state}#wechat_redirect; window.location.href url;注意最后的#wechat_redirect这是微信要求的固定锚点不能省略。这个方案的优势是零依赖、实现快适合对登录体验要求不高的内部系统。缺点是用户会离开自己的登录页跳到微信的页面扫完码再跳回来整个流程的“品牌感”很差。3.2 方案二在自己的页面内嵌二维码推荐大部分正式项目不会让用户跳走更常见的是在自己的登录页里直接渲染一个二维码。微信官方提供了WxLogin这个 JS 对象用法很直接。先引入官方脚本script srchttps://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js/script然后在页面放一个容器div idwechat-login-container/div接着初始化二维码const loginObj new WxLogin({ self_redirect: true, id: wechat-login-container, appid: wx1234567890abcdef, scope: snsapi_login, redirect_uri: encodeURIComponent(https://your-site.example.com/auth/wechat/callback), state: Math.random().toString(36).slice(2), style: , href: });self_redirect这个参数很关键。设置为true时用户扫码授权后当前页面会直接跳转到回调地址体验最顺滑。设置为false时微信会在扫码后展示一个中间提示页用户还得再确认一次多一步操作就没那么顺畅了。内嵌方案的好处不只是好看还方便做自定义。比如在二维码旁边放操作指引文案、倒计时提示、刷新按钮这些是直接跳转方案做不到的。实际操作中有一个细节需要注意如果页面上已经渲染过一次二维码再次调用new WxLogin前一定要清空容器内容。我有一次没清空就重复初始化结果两个二维码叠在一起用户拿手机扫了半天没反应。3.3 扫码确认后的前端回调处理用户扫码确认后微信会把浏览器重定向到redirect_uri并在地址栏加上code和state两个参数。这个回调地址可以是一个后端接口也可以是一个前端页面建议直接指向后端接口避免前端绕一圈。如果确实需要由前端页面接收回调页里要做的事很简单const params new URLSearchParams(window.location.search); const code params.get(code); const state params.get(state); if (!code || !state) { // 用户取消授权或直接访问了这个页面 window.location.href /login?error1; return; } const resp await fetch(/api/auth/wechat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ code, state }) }); const data await resp.json(); if (data.token) { localStorage.setItem(token, data.token); window.location.href /dashboard; } else { window.location.href /login?error2; }核心原则是前端只负责把code和state原样转交给后端所有校验逻辑都在后端完成。前端拿code做任何操作都是多余的还可能引发安全问题。3.4 二维码过期、刷新与用户误操作的处理微信扫码登录的二维码是有有效期的我实测下来大约五分钟左右。二维码过期后页面不会自动给你提示需要前端自己处理。推荐在渲染二维码的同时启动一个倒计时时间到就清空容器、重新生成state、重新调用WxLogin。这里有一个很常见的用户误操作场景用户扫了旧二维码但一直没确认这时候你刷新了页面生成了新二维码。用户再确认时微信回调带回来的是旧 code后端一查 state 对不上就会报“二维码已失效”。所以在回调接口里遇到 state 不匹配的情况不要直接返回错误页而是引导用户回到登录页重新扫码。另外如果是 SPA 应用回调地址千万别配到前端路由上。因为前端路由只是换了个页面组件并没有把 code 送到后端。我见过有人把回调地址配成https://xxx.com/login?fromwechat然后在登录页里发请求结果导致回调链路复杂且容易丢失参数。正确的做法是配一个独立的后端接口地址后端处理完再重定向到前端页面。4. 后端接入code 换 token、用户信息与登录态4.1 回调接口的设计与 state 校验后端回调是整个流程的“中枢”。以 Python 的风格写个伪代码示意app.route(/auth/wechat/callback) def wechat_callback(): code request.args.get(code) state request.args.get(state) # 1. 校验 state cached_state session.get(wechat_state) if not state or state ! cached_state: return redirect(/login?errorstate_invalid) # 2. 用 code 换 access_token token_info wechat_exchange_code(code) # 3. 拉取用户信息 user_info wechat_get_user_info(token_info) # 4. 绑定或创建用户 user bind_or_create_user(user_info) # 5. 签发自家登录态并跳转 token generate_login_token(user) return redirect(f/login/success?token{token})state校验建议放在最前面后面所有操作都建立在“这是一次有效授权请求”的前提下。state可以存在 session 或 Redis 里有效期设十分钟足够不用太长。4.2 用 code 换 access_token这一步是后端最核心的调用接口如下GET https://api.weixin.qq.com/sns/oauth2/access_token?appidAPPIDsecretSECRETcodeCODEgrant_typeauthorization_code响应示例{ access_token: ACCESS_TOKEN, expires_in: 7200, refresh_token: REFRESH_TOKEN, openid: oXXXXX, scope: snsapi_login, unionid: uXXXXX }这里有三个容易出错的地方我分别说下code是一次性的。换过一次 access_token 之后就失效。如果用户扫码后刷新了页面导致同一个 code 被请求两次第二次微信会返回“invalid code”之类的错误。后端要做好幂等处理最简单的办法是在 Redis 里用SETNX code记录重复请求直接返回同一结果。这里的接口是https://api.weixin.qq.com/sns/oauth2/access_token不是https://api.weixin.qq.com/cgi-bin/token。后者是获取全局 access_token 的接口用于调用服务端 API很多刚接的人会搞混结果拿到的 token 根本不能用。access_token的有效期是 7200 秒也就是两小时。但实际开发中我发现它在用于sns/userinfo时偶尔会提前失效所以拉取用户信息时最好对错误码做判断遇到 40001 或 42001 就尝试用refresh_token刷新一次。4.3 拉取用户头像昵称拿到 access_token 后再调用户信息接口GET https://api.weixin.qq.com/sns/userinfo?access_tokenACCESS_TOKENopenidOPENIDlangzh_CN响应示例{ openid: oXXXXX, nickname: 昵称, sex: 1, province: 广东, city: 深圳, country: 中国, headimgurl: https://thirdwx.qlogo.cn/..., unionid: uXXXXX }注意这个接口是sns/userinfo不是cgi-bin/user/info。前者是 OAuth2.0 网页授权体系下的用户信息接口后者是公众号管理后台的接口两者权限和数据来源都不一样。关于unionid多说一句。如果同一个开放平台账号下绑定了多个应用比如一个网站应用加一个小程序那么同一个微信用户在这些应用里的unionid是相同的。这对做用户统一是很重要的字段建议入库存储时优先用unionid作为跨端识别标识而不是只靠openid因为不同应用的openid是不一致的。头像地址是临时 CDN 链接有可能过期或失效。需要持久化用户头像的话最好把图片拉下来存到自己的存储服务里或者定期刷新。4.4 用户绑定与登录态签发扫码登录拿到的微信身份和自家系统里的账号体系是两套东西。第一次扫进来的用户系统里肯定没有对应记录需要处理“绑定”逻辑。常用的方案有两种自动创建账号以unionid或openid作为唯一标识首次扫码直接创建一个账号并登录。适合不强制要求手机号的场景。引导绑定已有账号首次扫码发现系统里没有对应用户时跳到“绑定手机号”或“输入用户名密码”页面绑定成功后以后再扫就自动登录。我实际做过的系统大多采用第二种因为业务上需要手机号关联订单或其他业务数据。数据表可以设计得比较简单user( id, username, phone, openid, unionid, created_at, updated_at )签发登录态时建议和自家系统已有的登录入口统一。如果其他登录方式用的是 JWT扫码登录也发 JWT不要单独搞一套。我一般这样签发import jwt import time token jwt.encode( { user_id: user.id, unionid: user.unionid, exp: int(time.time()) 7200 }, SECRET_KEY, algorithmHS256 )买粉丝登录只负责“确认微信侧身份”登录之后的会话过期、权限控制、用户状态等都按自家系统的规则走。这样后续维护不会出现两套逻辑打架的情况。5. 排坑实录六个高频问题与定位思路5.1 一直报 redirect_uri 参数错误这个报错是扫码登录里出现频率最高的。对照下面几个方向排查排查点具体操作后台回调查询看开放平台后台配置的域名是否与 redirect_uri 域名完全一致协议头页面是 httpsredirect_uri 配成了 http或者反过来URL 编码是否对 redirect_uri 做了 encodeURIComponent回调域格式后台填的是域名不填协议、路径和端口AppID 来源是不是拿了公众号 AppID 或小程序 AppID 来用我排查的时候习惯先在浏览器地址栏里直接看生成的完整授权 URL一眼就能看出 redirect_uri 有没有被正确编码。那个完整链接里redirect_uri如果显示成一长串%3A%2F%2F才是正常的如果出现://说明没有编码。5.2 二维码不显示或白屏内嵌二维码方案里白屏问题通常有三种原因wxLogin.js没加载成功。打开浏览器控制台看网络面板确认这个外链脚本是否正常返回。容器id对应元素还不存在脚本就执行了。尤其注意如果把脚本放在页面头部而容器写在 body 后面就会找不到元素。重复初始化没有先清空容器。还有一个容易被忽略的页面必须是 https。微信官方对非安全域名的限制比较严格我用 http 的测试环境调试时二维码就经常加载不出来。5.3 本地开发怎么联调回调微信扫码登录不支持localhost回调这是本地开发最大的障碍。实操中有三种做法用测试域名加上内网穿透工具把本地服务映射到公网临时地址。注意临时地址会变每次变了都要回开放平台后台更新回调域。将回调接口直接部署到测试服务器让本地前端把 redirect_uri 指向测试环境的后端地址前后端分离开发时比较常见。如果公司有现成的公网测试环境直接在测试子域名上部署一整套环境。不管用哪种方式调试完一定要记得把正式环境的回调域切回来。我有一个同事在测试环境联调完把临时域名带到了生产配置里第二天线上扫码全部回调失败排查了半天才发现是配置没切回去。5.4 提示 code 已使用或已过期这个问题通常不是微信那边出了故障而是你的后端对同一个 code 处理了多次。典型场景是用户扫码授权后页面跳到了回调接口后端返回结果前用户按了浏览器刷新于是同一个 code 又被请求了一次。解决方法是给 code 加“一次性”约束。在 Redis 里用SET code_id 1 EX 300做一个标记第二次请求发现 key 已存在就直接返回“请重新扫码”的提示而不是继续去微信接口换 token。还有一种情况是 code 虽未被使用但用户隔了很长时间才确认扫码code 已经过期。遇到这种情况前端应该根据后端的错误码做友好提示让用户重新生成二维码。5.5 拿不到头像昵称或 unionid先说unionid。如果返回的数据里没有unionid最常见的原因是开放平台账号没有绑定你创建的这个网站应用。开放平台的“账号管理”里有个绑定应用的功能很多应用创建完没有去绑定导致微信不认识它们是同一账号体系下的应用。再说头像昵称。确认你调用的是sns/userinfo而不是cgi-bin/user/info。前者的 access_token 来自sns/oauth2/access_token后者的 token 来自cgi-bin/token两个 token 不能混用。还有一个小概率情况用户授权时如果只给了基础权限或者头像因微信策略暂时无法返回headimgurl可能为空。后端保存用户资料时要做默认值兜底否则前端展示用户头像的地方会裂图。5.6 实现中容易忽略的体验细节除了技术问题扫码登录的交互细节也值得上心。我常用的几个习惯扫码确认后回调接口还没返回前页面给一个“已授权正在登录”的过渡状态避免用户以为卡死了。二维码旁边放刷新按钮倒计时结束或者用户反馈扫不了时能主动刷新比强制刷新页面更好。后端回调接口打详细日志把 code、state、微信返回的原始内容都记录下来。这些日志是排查“神秘问题”的第一手材料。我个人在实际操作中的体会是扫码登录最难的不是把二维码放出来而是想清楚 code、state、access_token、openid 这些凭证在什么环节、由谁保存、由谁校验。只要把前端与后端的边界理清楚AppSecret 不下发、code 只换一次、state 每次独立生成再配合一套好用的回调日志绝大多数问题都能在十分钟内定位。最后再分享一个小技巧联调时可以在回调接口把微信返回的原始参数和响应体完整打印出来存成固定格式的日志很多“看上去看不懂”的异常一翻日志就真相大白了。