
从“文档看起来很简单”到真正跑通微信扫码登录中间隔着一整条踩坑链路。这篇内容我会把网页接入微信扫描二维码登录的完整过程拆开讲包含账号侧配置、授权URL构造、前端轮询、回调换token以及那些文档里不会写明的边界情况和排查思路给正在接这个功能的后端或前端同学当一份可直接参考的落地笔记。1. 扫码登录的第一步弄清楚你走的是哪条授权通道在写任何代码之前先搞清楚一件事网页上的“微信扫码登录”和你平时在手机上点“微信授权登录”不是同一套通道。手机端用的是App内的OAuth授权网页端用的是开放平台的“网站应用”扫码登录两者在申请入口、回调域名校验、拿到的用户标识字段上都有差别。1.1 开放平台账号与应用登记的硬门槛要做网页扫码登录第一步要在微信开放平台注册开发者账号然后创建一个“网站应用”。这个环节听起来平淡但卡住不少人的是开放平台账号需要完成开发者资质认证没有认证前只能创建测试应用。网站应用创建时一定要选对应用类型“网站应用”和“移动应用”的授权流程不同选错了后续拿到的授权回调地址格式都会不一样。审核通过后你会拿到一个AppID和一个AppSecret前者是公开的后者必须藏在服务端。我见过有人拿公众号的AppID直接来对接网页扫码登录结果是扫码后提示“该应用不存在”或者一直回调失败。公众号开放平台和网站应用是两个入口凭据不能混用。提示如果你只是想先验证流程完全可以先用测试号的配置把二维码和回调链路跑通确认无误后再替换成正式应用的AppID。这个顺序能帮你省去很多因为资质审核进度卡住的等待时间。1.2 回调域名的校验逻辑为什么本地调试那么难受网站应用创建时会要求填写“授权回调域名”。这个域名的校验非常严格有几个容易忽略的细节回调域名必须是ICP备案过的域名且不能带协议头不能带路径比如https://example.com/wechat/callback这种写法是不行的。域名解析生效后微信服务器会去检查你的域名是否可访问所以本地localhost是永远无法通过校验的。同一域名只能被一个应用绑定如果一个域名在别的主体名下已经绑了别的应用你就会遇到“域名已被占用”的提示。本地调试的解决方案我一般是这么处理的在服务器上配一个Nginx反向代理把/oauth路径转发到本地开发机的某个端口然后本地改hosts把回调域名解析到本机这样既满足微信服务器的回调解析又能保持本地热调试。nginx.conf 中的一个典型配置片段 server { listen 80; server_name oauth.example.com; location /oauth/wechat/callback { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }本地机器的回调接口跑在8080端口微信后台的回调域名填oauth.example.com这样就可以在开发环境里完整走一遍扫码回调和code换token的流程。1.3 获取相关凭证AppID与AppSecret的用途边界很多团队在对接时会犯一个经典错误在前端代码里拼接授权URL时把AppSecret也带上。AppSecret的用途只有一个——服务端调用接口换取access_token和刷新凭证任何出现在前端、被浏览器直接访问的代码里的AppSecret都视为泄露。正确使用的边界我整理成一张表凭证可出现在前端核心用途过期策略AppID是构造授权URL、识别应用身份长期有效AppSecret否服务端换取access_token长期有效但可重置access_token否调用用户信息等接口2小时有效需刷新refresh_token否刷新access_token30天可被替换需要特别注意的一个点扫码登录时拿到的access_token和用户openid是针对“该网站应用”维度的不是公众号维度的。后续如果要在服务端发模板消息或者调用公众号接口那是另一套凭证体系不要混着用。2. 授权URL的构造与二维码生成看似简单但容易拼错准备工作做完真正进入技术环节。首先要理解整个登录链路的第一棒构造一个让用户去扫码的授权链接。2.1 授权URL每个参数的含义微信网页扫码登录的授权URL通常是这样一段https://open.weixin.qq.com/connect/qrconnect?appidYOUR_APP_IDredirect_urihttps%3A%2F%2Foauth.example.com%2Foauth%2Fwechat%2Fcallbackresponse_typecodescopesnsapi_loginstateSTATE_VALUE#wechat_redirect每个参数的作用我逐一拆解一下appid应用唯一标识对应开放平台网站应用的AppID。redirect_uri微信授权完成后的回调地址必须与后台配置的域名同源并且需要URL编码。这里最容易出错的是编码不规范比如忘了把冒号和斜杠转成%3A%2F%2F导致扫码后跳转地址解析异常。response_type固定值code表示以授权码形式返回。scope网页扫码登录固定为snsapi_login。如果你在手机端App里用snsapi_userinfo那套网页端往往拿到不到预期结果。state开发者自定义参数用户扫码确认后回跳时会原样带上。它的核心作用是防CSRF和标记登录来源。#wechat_redirect固定的锚点标记微信端用来识别这是一个网页授权跳转。我见过有人把scope传成snsapi_base结果用户扫码后虽然没有报错但回调里拿不到用户的基础信息。这个参数没有任何可模糊的空间必须按文档语义来。2.2 前端二维码渲染的两种形态拿到授权URL之后的落地形态有两种第一种是页面跳转式。点击“微信登录”按钮后浏览器直接跳转到授权URL用户在微信确认页完成扫码和确认再自动回跳到回调地址。这种方式实现最省事但体验上有一个割裂感用户离开了你的页面。第二种是内嵌二维码式。把授权URL生成成一张二维码展示在自己的登录框旁边。用户用微信扫码后手机端确认PC浏览器再通过轮询触发后续登录逻辑。这才是大家通常理解的“网页扫码登录”的完整形态。第二种方案中前端生成二维码常见的有两种选型用前端库qrcode或qrcodejs2把授权URL绘制成Canvas或图片后端生成二维码图片流前端直接用img展示。我自己的习惯是前端生成因为授权URL本身就是一段字符串前端构造更方便还能顺带把state随机数在生成的同时写入内存链路更短。// 以 qrcode 库为例前端生成二维码 import QRCode from qrcode; import { v4 as uuidv4 } from uuid; const state uuidv4(); sessionStorage.setItem(wxLoginState, state); const redirectUri encodeURIComponent(https://oauth.example.com/oauth/wechat/callback); const url https://open.weixin.qq.com/connect/qrconnect?appid${appId}redirect_uri${redirectUri}response_typecodescopesnsapi_loginstate${state}#wechat_redirect; QRCode.toCanvas(document.getElementById(qrCanvas), url, { width: 280, margin: 1 });2.3 state参数与防CSRF很多人忽略但必须做state不是微信给你的附加题它是对登录链路安全性的基础要求。攻击者可以把一个合法授权URL替换成自己构造的回调诱导用户扫码后让浏览器带上认证凭据跳到攻击者指定的地址而没有校验state的服务端会直接把这个code当成有效登录凭证处理。正确的做法是前端在生成二维码时把state存进sessionStorage回调接收时服务端也要校验state值是否在可信任的会话中出现过比较state一致后才继续换token。有一种常见错误是只在服务端生成并使用纯粹的随机数但前端生成二维码与回调校验不在同一会话导致state丢失回调时验证永远不通过。解决方式是生成state后同时下发到前端并关联到会话上下文。用uuid生成唯一值比自增数字和简单时间戳要可靠得多。3. 前端轮询状态扫码、确认、取消三态驱动整条登录链路二维码展示出来之后所有的工作并没有结束。用户扫码后你要怎么知道他已经扫了、确认了、还是取消了这就是前端轮询机制存在的意义。3.1 浏览器侧的轮询设计以state为标识的请求模型在二维码生成时前端为本次登录生成一个唯一标识loginTicket同时在后端缓存里存一条待登录记录。前端每隔1.5秒到2秒轮询一次服务端接口GET /api/login/status?ticketabc123后端基于这张ticket对应的状态返回不同结果。在实践里我习惯把状态定义成三种状态值含义前端表现0等待扫码二维码保持原样提示“请使用微信扫码”1已扫码未确认二维码区域变灰提示“已扫码请在手机端确认”2已确认触发后续登录态写入跳转首页或返回上一页-1二维码过期或已取消提示“二维码已失效请刷新”轮询不能设计成无上限的常规做法是设置120秒的超时时间。微信官方二维码的有效期通常也在这个范围内超过后无条件终止轮询并展示刷新按钮。轮询接口本身也要设置超时保护用AbortController在组件销毁时中止上一次未完成的请求避免页面切走之后还在疯狂发送请求。3.2 扫码后未确认与已确认的界面切换这一节看着像前端UI问题但实际上后端状态机的状态流转才是核心。后端需要准备一张登录记录表字段至少包含ticket唯一标识前端轮询时携带state本次登录的防伪参数scene本次登录来源比如web端、电脑端某个页面status记录状态的变化openid扫码后写入的用户标识created_at记录生成时间用于过期清理。用户扫码后微信会把一个临时code回调到你的后端服务后端这时还不能立刻给前端返回“登录成功”因为用户可能扫了码但没在手机上点“确认”。微信在扫完二维码和点击确认之间有一个明确的时间差后端需要在这个窗口期更新状态为“已扫码未确认”然后再等微信用户最终点击的结果。一旦用户在手机上点了确认微信会再次回调开发者服务器此时才能更新状态为“已确认”并执行token换取和用户信息拉取。这个时序如果不理解会出现一种非常常见的bug用户刚扫码前端立刻显示登录成功但服务端实际还没拿到用户身份随后某次刷新又变回未登录。3.3 超时与刷新过期二维码怎么处理二维码过期听起来是小事但处理不好很容易引用户投诉。最典型的场景是用户拿着手机对着屏幕等了一会儿然后去接了个电话回来再扫发现扫不出来或者扫完老是失败。这时如果你没有主动提示二维码过期用户会认为是系统坏了。所以在轮询超时之后除了前端要展示“刷新二维码”按钮后端也要把对应的ticket标记成失效。另外还要清理掉无效的轮询记录避免每天堆积大量脏数据。我在后端写过一个定时任务每10分钟扫一次表把超过2分钟且未完成状态流转的记录批量置为最终失效。这类清理看起来无关紧要但日活过万之后脏数据积累的速度会超乎想象还会影响登录状态统计的准确性。前端刷新二维码时有一个关键动作是同步刷新state。如果你沿用同一个state重新请求授权URL前面提到CSRF风险就先不说了更重要的是旧二维码已经被人扫过用户以为已经登录成功但在回调校验时state已经被消费掉新二维码也会陷入验证通过不了的死循环。4. 回调与code换token登录态写在哪一层二维码前端轮询跑通只是完成了“扫码动作的捕获”真正决定登录成败的是服务端的回调和token处理。4.1 服务端回调的处理顺序微信回调你的服务器时URL结构类似这样/oauth/wechat/callback?codeACCESS_CODEstateSTATE_VALUE后端收到请求后处理顺序应该严格遵守以下几步先校验state与本次会话是否匹配不匹配直接拒绝。利用code向微信服务器请求access_token。用access_token换取用户基础信息包含openid、unionid、昵称、头像。在自己的用户体系里查找或创建用户记录。生成本站点登录态写入session或返回token给前端。把轮询状态更新为成功同时把本次code标记为已消费。第二步和第三步使用的接口分别是POST https://api.weixin.qq.com/sns/oauth2/access_token?appidAPPIDsecretSECRETcodeCODEgrant_typeauthorization_codeGET https://api.weixin.qq.com/sns/userinfo?access_tokenACCESS_TOKENopenidOPENIDlangzh_CN有个细节code换取access_token的接口必须由服务端发起绝不能放在前端。因为接口返回的access_token能直接操作用户数据一旦暴露在网络请求里结果就是用户隐私泄露事故。4.2 openid与unionid的区分网页扫码登录回调返回的用户标识是openid它只在该网站应用内部唯一。同一个用户在你的网站应用里的openid和在公众号里的openid是不同的。如果你们的业务体系里有公众号、小程序、App等多个微信入口这个时候就要特别注意unionid的打通问题。开放平台下的不同应用只要绑定了同一开放平台账号主体用户授权后都会返回同一个unionid。所以我在用户表里通常设计两个字段wechat_openid存当前应用下用户标识wechat_unionid存跨应用统一标识两者都加唯一索引。二维码登录时优先用unionid匹配存量用户匹配不到时再用openid建新用户。这样做的实际价值是如果用户在公众号里已经绑定过手机号那么在网页扫码时即使他第一次来到你的网站也能通过unionid直接关联到同一个账户不需要再让用户填一遍手机号或邮箱。4.3 token与登录态存储的设计扫码成功之后怎么发登录态是个有争议的问题。网上有很多方案基于同一套后端我常用的方式是服务端生成一个随机token关联用户ID和过期时间存到Redis里前端把token写到httpOnly的Cookie里前端JavaScript不可读只能靠浏览器自动携带。这种设计的好处是抵御XSS获取token的风险。如果你选择了localStorage存储token后续在开发中就要额外做很多XSS防护比如过滤用户输入、注意富文本渲染组件等。我在一个老项目里吃过这个亏在一次代码扫描后发现某个第三方组件存在注入风险幸好当时token存的是httpOnly Cookie攻击者没法直接读取到。从那之后新项目一律采用httpOnly Cookie方案。Redis里存的session结构我是这样设计的login:token:{token} - { uid: 12345, expireAt: 1710000000 }每次请求过来服务端中间件先查一次Redis检查是否存在且未过期。过期时间一般设置为7天如果用户勾选了“记住我”可以延长到30天。5. 我反复踩过的坑与高频必查清单这块内容是实际对接中反复折腾过的case每一项都是真实事故现场复盘读完可以直接对照排查。5.1 无法显示二维码或二维码过期二维码不出现首先要排查的不是前端代码而是授权URL本身。直接在浏览器里打开你拼好的完整链接如果跳转到微信开放平台的页面并且显示二维码说明前端渲染没问题如果提示参数错误或跳转到404优先检查AppID是不是开放平台网站应用的IDredirect_uri是否做了URL编码是否和后台配置的域名同源scope是否传的snsapi_login。二维码一闪而过或扫了立即提示过期则多半是后端缓存设置太短。我曾经把Redis里ticket的过期时间按普通缓存习惯设成30秒结果二维码从生成到用户扫码中间稍微迟疑一下就全部过期。最后调整为120秒同时保留一个定时清理任务兜底。5.2 扫码后页面没有反应用户扫码后手机端已经提示确认成功但PC网页毫无变化。这类问题的根因分两种情况第一种是轮询没触发。检查前端代码里是否在二维码生成时启动了轮询以及轮询请求是否因为页面收到过其他全局错误被中止。第二种是轮询拿到了但状态没变。这时要看后端回调处理链路的日志。一个高频根因是回调接口在通过code换access_token时因为时间差问题拿不到有效凭证微信返回了40029code无效。这往往是回调URL在测试阶段被重复触发同一个code被用第二次。所以在处理逻辑里要主动对code做一次性消费管理在Redis里记录已使用的code重复调用直接拒绝。5.3 回调code重复使用报错这其实是上面那个问题的延伸但值得单独列出来讲。微信的授权code是一次性的使用过后立即失效有效期虽然官方说5分钟但用过一次就直接作废。如果用户扫码后不小心刷新了回调页或者浏览器插件拦截并重放了请求同一个code就会被重复用来换token。后端正确姿势是在收到回调时先用redis.get(wx_code_ code)检查是否已存在不存在则写入并设置60秒过期然后继续后续流程存在则直接返回一个页面提示“登录处理中请稍后刷新页面”不要再继续换token。这么处理后重复刷回调页也只会出现“处理中”提示而不会因为code无效导致整个登录链路失败。5.4 开放平台配置与回调的“最终一致性”问题这一点极其隐蔽但也极其致命。开放平台修改回调域名时微信官方文档说“立即生效”但实际操作中我发现往往是滞后的尤其是域名从A改成B后旧域名在短时间内依然能收到部分回调新域名反而时通时不通。解决方案是在改造期间新旧两个回调地址都保留一段时间两套地址的处理器都记录日志并落同一张处理表等确认新域名稳定运行几天后再把旧的处理路由下线。千万不要在配置变更的当天把所有流量切到新地址在一个线上项目里这样操作大概率会遇到一部分用户回调丢失。6. 个人实践经验拾遗最后补充几点在工程中沉淀下来的经验算是对前面的总体应用补充。先把日志打全再上线。从用户点击登录、二维码生成、轮询发出、微信回调进入、code换取token成功、用户信息返回、会话创建成功每一步都输出一条结构化日志。尽早把日志打全排查问题的时间至少省一半。我接手过一个历史遗留项目回调接口里连最基本的“收到回调了没有”都没打日志出问题只能靠抓包猜测非常痛苦。测试环境单独准备一套回调配置。不要在生产扫码登录还没验证完的时候就顺手改了生产环境的回调域名为测试域名。我在一次联调中因为图省事共用了一套配置导致测试同学扫码后误登了生产环境账号数据污染问题折腾了一整天才解决。测试环境务必使用独立的AppID和回调域名代码里通过环境变量区分。给登录链路加一个全局限流。二维码生成接口和轮询接口虽然单次请求成本不高但被脚本刷起来也很消耗资源。建议对生成二维码的接口按IP限制每分钟10次对轮询接口按IP限制每分钟30次。刷得太频繁的直接拒绝或延时处理保护后端不至于在登录高峰期被误伤。用测试号先跑通全链路再换正式参数。测试号不需要复杂的资质审核流程可以在几分钟内完成配置非常适合用来验证你的回调域名、后端路由、token读写逻辑是否正确。主链路没问题后再切换到正式AppID能极大缩短从开发到上线的周期。扫码登录这个功能单纯看文档会觉得只是一个跳转加回调但真正落地后才会发现前端的二维码渲染、后端的code消费管理、token存储安全、回调配置的生效滞后每一个环节都有足够的细节去打磨。把一个高频入口做成“用户无感、后端可控、故障可查”的稳定能力需要把上面这些看起来琐碎的环节都考虑进去。这套流程跑通之后无论是后续接入小程序码登录还是公众号内登录都有现成的思路可以复用。