ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PC网站微信扫码登录实战:从二维码到登录态全流程解析

PC网站微信扫码登录实战:从二维码到登录态全流程解析 PC网站实现微信扫码登录功能二接上一篇开放平台账号、网站应用、回调域名这些前置条件都搞定了也拿到了 AppID 和 AppSecret但很多朋友卡在了“申请通过之后到底怎么写代码”这一步。本篇直接进入实战把 PC 端扫码登录从二维码展示、状态轮询、回调处理、换取用户信息到登录态建立的完整链路全部串起来顺便把我实际联调中踩过的坑一并抖出来。先说清楚一个很多人搞混的点微信扫码登录用的是微信开放平台的“网站应用”能力不是公众号后台的“网页授权”。前者拿到的 AppID 是开放平台应用的后者是公众号的两者接口地址不同、权限体系不同甚至回调域名校验逻辑也有差异。如果你拿公众号的 AppID 去调 https://open.weixin.qq.com/connect/qrconnect 这个地址微信会直接报错。所以开工前确认两件事第一你手里的 AppID 来自 open.weixin.qq.com第二该应用已经通过了“网站应用”审核并且配置了授权回调域。1. 扫码登录的完整链路与整体设计思路1.1 一次扫码登录背后发生了什么微信扫码登录不是简单的“扫一下就行”它背后是一条完整的数据链路。我把这条链路拆成五步大家心里有数之后写代码时就知道每一步该干什么前端页面请求后端后端生成一个跳转微信授权页的 URL前端拿到 URL 后通过 iframe 或重定向打开微信官方的二维码页面。用户用手机微信扫码微信端展示“确认登录”页面用户点击确认。微信服务器通过浏览器重定向到你在开放平台配置的回调地址并在 URL 上附带一个code参数。这里的 code 是一次性的、有效期大概 5 分钟。后端用 code 向微信服务器换取access_token和openid。这个 access_token 是“用户授权 access_token”和公众号接口调用的 access_token 完全两码事。后端用access_token和openid拉取用户基本信息头像、昵称等再根据 openid 查找或创建本地用户最后签发自己的登录态token、session 等返回给前端。整个过程的关键点在于微信只认 code不认别的。前端拿到 code 之后不能自己用因为它只存在于浏览器地址栏如果不经过后端直接用等于把换取用户信息的机会暴露给任何人。所以标准的做法是前端拿到 code 后立即传给后端后续所有和微信服务器的交互都放在后端完成。1.2 二维码由谁生成前端直接拼 URL还是后端返回扫码登录的二维码本质上就是微信官方那个授权页面的链接。有两种常见做法第一种前端直接拼 URL形如const url https://open.weixin.qq.com/connect/qrconnect ?appid appId redirect_uri encodeURIComponent(redirectUri) response_typecode scopesnsapi_login state state #wechat_redirect然后把这个 URL 放到 iframe 的 src 或者window.location.href上。这种做法简单直接适合纯静态页面或个人项目。问题在于appid、redirect_uri等参数暴露在前端代码里虽然appsecret不在其中但如果你想在跳转前做一些服务端校验或动态拼接逻辑前端拼串会非常别扭。第二种后端生成跳转 URL 并返回给前端。前端只需要拿到一个链接丢进 iframe 或直接跳转即可。这么做可以把 AppID、state 生成逻辑、回调地址管理等统一收敛在服务端后续如果要加日志、加埋点、加安全校验都在后端完成灵活度和安全性都更高。我倾向于第二种做法。实际项目中前端打开登录弹窗时先向后端发一个GET /api/login/qrcode后端返回类似这样的数据{ code: 0, data: { qrUrl: https://open.weixin.qq.com/connect/qrconnect?appid...redirect_uri...response_typecodescopesnsapi_loginstateabc123#wechat_redirect, state: abc123, expireAt: 1699999999999 } }这样做的额外好处是后端可以把 state 值存起来回调时比对防止 CSRF 攻击。前端拿到的qrUrl直接放进 iframe简单干净。1.3 轮询二维码状态不做等死做了才有体验用户扫码之后微信会主动跳转到回调地址但回调地址里只有 code 和 state并没有“用户已确认”这个中间态。如果用户扫了码但迟迟不点确认前端没有任何感知。所以要做状态轮询。这里的逻辑是这样前端在拿到 qrUrl 后同时启动一个定时器每隔 23 秒向后端询问当前 state 对应的登录状态。后端通过 session 或 Redis 维护一份状态表回调接口收到微信带 code 的请求后把对应 state 的状态标记为“已回调”。前端轮询接口返回的状态变化再决定是继续展示二维码、提示“已扫码待确认”、还是直接跳转进入系统。有的团队图省事直接让前端监听 iframe 的 URL 变化但 iframe 跨域时无法读取内部地址微信的二维码页面本身也禁止跨域操作所以轮询基本是标配。轮询间隔建议 23 秒太频繁会给后端增加无谓压力太慢用户会感觉卡顿。实测下来 2.5 秒是比较舒服的区间。2. 前端实现Vue 页面中二维码的展示与状态监听2.1 弹窗组件结构设计这里我以一个 Vue 3 Composition API 的登录弹窗为例。组件拆成两部分外层弹窗 内层二维码区域。核心逻辑集中在一个useWechatLogin方法里。先看模板部分template div classlogin-modal v-ifvisible div classmodal-mask clickcloseModal/div div classmodal-body div classqr-container v-ifqrUrl iframe :srcqrUrl classqr-frame scrollingno / div classqr-status v-ifpollingStatus scanned p已扫码请在手机上确认/p /div div classqr-status v-else-ifpollingStatus expired p二维码已过期/p button clickrefreshQrCode刷新二维码/button /div /div div classqr-placeholder v-else p二维码加载中.../p /div /div /div /template这里有个细节微信二维码页面本身是有一定高度的官方推荐 iframe 宽度 300px、高度 400px。但实际渲染时不同浏览器对 iframe 的内边距和外边距处理有差异建议给 iframe 加上frameborder0和scrollingno并在外层容器固定尺寸避免出现滚动条或者白边。我习惯把 iframe 宽高设为 300×400外层容器设为 320×420 并居中视觉效果最接近官网示例。2.2 二维码加载与轮询逻辑方法部分的核心流程是打开弹窗时调用fetchQrCode→ 拿到 qrUrl 和 state → 启动轮询 → 轮询命中已登录则关闭弹窗并跳转 → 轮询命中过期则停止并提示。const fetchQrCode async () { pollingStatus.value loading const { data } await api.get(/api/login/qrcode) qrUrl.value data.qrUrl state.value data.state startPolling() } const startPolling () { clearInterval(timer) timer setInterval(async () { try { const { data } await api.get(/api/login/status, { params: { state: state.value } }) if (data.status logged_in) { clearInterval(timer) handleLoginSuccess(data.token) } else if (data.status expired) { clearInterval(timer) pollingStatus.value expired } else if (data.status scanned) { pollingStatus.value scanned } } catch (e) { // 网络异常忽略等下一轮 } }, 2500) }轮询状态我总共定义了四种loading二维码加载中、waiting正常等待扫码、scanned已扫码待确认、expired已过期。有些项目还会加confirmed状态但其实confirmed等同于后端已经收到了微信回调轮询接口会返回登录成功或 token不需要单独处理。scanned这个状态是怎么知道的微信本身并不会直接告诉你的后端“用户扫了但没确认”。实践中常见做法是后端在收到微信回调后才知道用户已经确认。所以“已扫码待确认”这个状态严格来说不是一个实时推送而是后端在开放平台接口文档里定义的“微信扫码后但未确认”只有微信端自己知道。准确说此状态需要后端配合微信在用户扫码后、确认前会先向配置的redirect_uri发一次请求带上code实际上不是微信的扫码登录流程中code只有在用户确认后才回传。所以这里要说明——如果产品确实需要“已扫码”提示需要另外用微信的“扫码授权事件推送”或结合轮询策略折中处理一般小项目不必纠结展示二维码后等待回调即可。这里要纠正一个常见误解很多人以为微信扫码后 iframe 内会发生变化从而可以判断“已扫码”。其实用户在手机上扫了码之后PC 端微信的二维码页面没有任何视觉变化用户必须到手机点“确认”确认之后 PC 端才会跳转。所以如果你不做后端配合仅靠前端是感知不到“已扫码待确认”这个中间态的。我的经验是如果产品没有强需求直接省略这个状态只保留“等待扫码”和“登录成功/失败”两个状态即可省掉大量复杂度如果确实需要“已扫码”提示需要借助微信的扫码授权事件推送但那需要额外配置性价比不高。2.3 路由跳转方案直接跳认证地址还是 iframe 嵌套确定了 qrUrl 之后前端有两种展示方式window.open或location.href直接跳转以及在页面内嵌 iframe。直接跳转的好处是流程最简单微信授权页占据整个页面用户扫完码确认后自动回到回调地址。缺点也很明显页面跳来跳去体验割裂而且如果回调地址是某个路由后端处理完再跳回来中间可能出现白屏等待。另外location.href跳转后前端已保存的 vuex 状态可能会因为整页刷新而丢失需要用redirect参数或 sessionStorage 暂存。iframe 嵌套的好处是弹窗不关闭用户扫码后停留原页面体验连贯。但 iframe 也有坑个别浏览器安全策略禁止跨域 iframe 的 localStorage 操作尤其是 Safari 默认对第三方 Cookie 和 localStorage 非常严格如果登录逻辑里依赖前端存储就会有隐患。所以需要权衡——如果是内部管理系统用 iframe 没问题如果是面向 C 端的门户网站建议直接跳转接受页面刷新带来的状态重置用 URL 参数弥补。我后来在面向 C 端的项目里几乎全部改成直接跳转流程更简洁遇到兼容性问题的概率也小得多。作为示例下面这段代码是直接跳转方案// 直接跳转到微信授权页 window.location.href data.qrUrl用户确认后微信重定向到回调地址例如/login/callback?codexxxstateyyy这时候前端路由匹配到回调组件取出 code 和 state发给后端换取登录态然后router.replace(/index)跳回首页。唯一的要点是回调组件必须在路由配置里提前声明而且要处理页面刷新导致 vuex 数据丢失的情况建议把“是否已登录”的信息交给后端返回的 token 去判断。3. 后端实现回调接口、用户信息获取与登录态建立3.1 回调接口接收 code并安全地换取 token微信在用户确认后会把用户重定向到你在开放平台填写的redirect_uri并拼接code和state。你需要在后端提供两个接口一个给前端轮询状态用一个处理微信回调。这里有一个容易混淆的点前端拿到的qrUrl里的redirect_uri应该指向后端的一个接口而不是前端路由。举个例子我在项目里是这样配置的开放平台回调域名https://api.example.com完整回调地址https://api.example.com/api/login/wechat/callback前端二维码里的redirect_uri就是这个后端接口地址。用户扫码确认后微信直接请求这个接口后端在这个接口里拿到 code 和 state。等后端处理完再重定向回前端页面比如// 后端伪代码 const handleWechatCallback async (req, res) { const { code, state } req.query // 1. 校验 state // 2. 用 code 换取 access_token 和 openid // 3. 拉取用户信息 // 4. 创建/更新本地用户 // 5. 生成登录 token // 6. 重定向回前端页面并在 URL 上携带 token res.redirect(https://www.example.com/login/callback?token${token}) }这里的关键设计是后端处理微信回调前端处理最终登录结果。微信的 code 不能经过前端否则有安全风险但登录成功后的 token 是给前端用的所以通过重定向 URL 传给前端最方便。回调接口的具体代码我用 Node.js (Express) 演示后端逻辑和语言无关换成 Java、Go、PHP 都一个思路const axios require(axios) async function getAccessToken(code) { const url https://api.weixin.qq.com/sns/oauth2/access_token const params { appid: WECHAT_APP_ID, secret: WECHAT_APP_SECRET, code, grant_type: authorization_code } const { data } await axios.get(url, { params }) if (data.errcode) { throw new Error(获取 access_token 失败: ${data.errcode} ${data.errmsg}) } return data // 包含 access_token, openid, refresh_token, scope }这里access_token不是普通接口令牌有效期默认 2 小时refresh_token有效期 30 天。这两个 token 都比较敏感不能放进响应里返回前端只能在后端留存或直接用于下一轮请求。如果只是做“扫码登录并拉取一次用户信息”拿到 access_token 后直接请求用户信息接口即可不需要持久化保存如果是做“绑定微信后长期推送消息”之类的需求就需要把 refresh_token 存库后续刷新 access_token。3.2 用 code 换路径注意区分 openid 与 unionid微信开放平台的扫码登录返回的用户身份标识是openid它是相对于该应用的唯一标识。同一个微信用户在不同应用比如你的网站、你名下的其他网站里的 openid 不同。如果你的公司有多个应用需要打通用户则要检查是否已申请并绑定开放平台账号下的“开放平台账号”这样才能拿到unionid。unionid 是同一个微信开放平台账号下所有应用通用的用户标识。我最初做的时候没注意这一点公司有两个系统用户分别扫码登录后端各存各的 openid造成同一个用户在两套系统里被识别成两个人。后来在开放平台后台把两个应用都绑定到同一个开放平台账号下并改用unionid作为唯一业务键才彻底解决。所以一开始设计用户表时就要考虑好openid字段是否要区分应用渠道是否预留unionid字段。用户信息接口返回的数据大体如下{ openid: o6_bmjrPTlm6_2sgVt7hMZOPfL2M, nickname: Band, sex: 1, language: zh_CN, city: 广州, province: 广东, country: 中国, headimgurl: http://thirdwx.qlogo.cn/mmopen/..., unionid: o6_bmjrPTlm6_2sgVt7hMZOPfL2M }注意headimgurl可能是 http 链接正式环境要转 https 或被浏览器拦截混合内容nickname可能含 emoji如果你用的 MySQL 且字符集是 utf8mb4存入数据前要确认链接层设置正确否则存 emoji 会报错。3.3 登录态建立openid 绑定、自动注册与自有 token拿到用户信息后接下来是业务层逻辑。常规做法是先查user_wechat_bind表看这个openid或unionid是否已经绑定过本地用户如果绑定了直接用本地用户 ID 签发登录态如果没有绑定再看用户有没有指定“扫码登录后自动注册”策略。如果允许自动注册就创建一个新用户同时插入绑定关系如果要求先登录 PC 端账号再绑定就需要设计一个“扫码后未绑定则跳转到绑定引导页”的流程。自动注册看起来方便但要注意垃圾账号问题。微信开放平台的scope只能拿到snsapi_login授权的用户信息不是所有用户都有unionid和完整资料。有些恶意用户批量扫码注册可能给你的用户表灌入大量无效数据。我见过一个做电商的朋友开放扫码后一周内用户表多了几千条“微信用户”脏数据全是空昵称、默认头像。后来加了风控规则新用户扫码自动创建后必须完成手机号验证才能下单。所以自动注册可以但建议对核心功能做门槛限制。登录态这块我一般用 JWT 或 session。用 JWT 的话生成 token 时把userId和openid放进 payload设置合理过期时间比如 1 天前端每次请求带上Authorization头后端中间件校验。不需要额外存 session特别适合前后端分离的 Vue 项目。生成 JWT 的伪代码如下const jwt require(jsonwebtoken) const token jwt.sign( { userId: user.id, openid: user.openid }, JWT_SECRET, { expiresIn: 1d } )3.4 state 参数必须要校验别偷懒微信官方文档对state参数的建议是用于防止 CSRF 攻击。实际开发中我见过不少项目把 state 写死成固定值甚至直接不加 state这都是安全隐患。攻击者可以构造一个恶意链接诱导已登录用户访问微信会带上一个合法的 code 回调到你的接口后端如果没有校验 state就会被攻击者利用实现登录态覆盖或账号绑定劫持。正确做法后端生成一个随机字符串作为 state存到 Redis 或内存 Map设置 5 分钟过期回调时取出请求中的 state和存储值比对不一致直接拒绝。key 可以用state本身value 存生成时间。校验通过后立刻删除防止重复使用。// 生成二维码接口 const state crypto.randomBytes(16).toString(hex) await redis.set(wx_login_state:${state}, Date.now(), EX, 300) res.json({ qrUrl: buildQrUrl(state), state }) // 回调接口 const savedState await redis.get(wx_login_state:${state}) if (!savedState) return res.status(403).send(state 无效或已过期) await redis.del(wx_login_state:${state})3.5 轮询接口设计状态机的简单实现后端需要一个给前端轮询的接口。我用的方案是在回调接口里拿到 code 换 token、拉取用户信息、建立登录态这些完成之后把state对应的登录结果写进 Redis比如await redis.set(wx_login_result:${state}, JSON.stringify({ status: success, token }), EX, 60)前端轮询时查询这个 keyapp.get(/api/login/status, async (req, res) { const { state } req.query const result await redis.get(wx_login_result:${state}) if (!result) return res.json({ status: waiting }) const data JSON.parse(result) // 返回成功之后立刻删除防止重复使用 await redis.del(wx_login_result:${state}) res.json(data) })这里要注意接口返回成功后要立刻删除 Redis 里的结果否则前端重复轮询或恶意请求同一个 state 可能拿到同一个 token造成会话重复创建。4. 常见问题与排查技巧实录我把实际联调中遇到的高频问题整理成一张速查表方便大家对照排查。这些问题里有些靠查官方文档就能解决但有几个坑官方文档写得模棱两可网上资料又互相抄我特意把它们拎出来说清楚。现象可能原因解决方案点击登录后微信提示“redirect_uri参数错误”回调地址与开放平台配置不一致或未 URL 编码核对redirect_uri与后台配置域名完全一致注意 HTTP/HTTPS 区别代码里先encodeURIComponent再拼入链接不要二次编码二维码能显示扫码后 PC 端无任何跳转手机确认后报错后端回调接口挂掉或回调接口访问超时查看后端日志直接用浏览器访问qrUrl手动走一遍流程观察微信回调是否到达后端拿到 code调换取 token 接口报40029code 已过期或重复使用code 有效期只有 5 分钟且只能消费一次检查是否前端拿 code 换过 token、后端又换了一次用 code 换 token 报40163code 已经被使用过说明你之前测试时已经消费了同一个 code重新扫一次生成新 code能拿到 openid但拿不到昵称头像授权 scope 不对或者用户微信设置了授权范围确认授权链接里scopesnsapi_login如果用户关注了公众号但没关注过开放平台可能部分资料取不到用默认值兜底openid 每次登录不一样用户在各应用间的 openid 不同属于正常理解微信开放平台机制想跨应用识别用户必须升 unionid扫码后提示“无法验证应用”或“该链接无法访问”AppID 归属错误或应用未通过审核到开放平台后台检查网站应用状态确认 AppID 属于开放平台站点应用而不是公众号前端轮询接口收到401token 已经过期或根本没生成检查后端登录态签发逻辑确认回调完成后 token 是否已正确写入 Redis 或 sessionSafari 下页面登录成功后刷新就退出localStorage/sessionStorage 被限制或 Cookie SameSite 策略确认前后端域名关系调整 Cookie 的 SameSiteNone; Secure 属性或将登录态存到 httponly Cookie4.1 redirect_uri 参数错误的隐蔽案例这个报错几乎每个人都遇到过但我遇到过一次特别隐蔽的情况redirect_uri在后台配置的是https://api.example.com/login/callback前端拼 URL 时也写对了结果还是报错。后来排查发现我在后端生成 qrUrl 时对redirect_uri做了encodeURIComponent然后拼接时又对整串 URL 做了一次encodeURIComponent导致redirect_uri被二次编码微信解析失败。正确做法是将完整的redirect_uri编码一次拼入 qrUrl整个 qrUrl 不再二次编码。在浏览器地址栏手动测试时你会看到 qrUrl 长这样是正常的https://open.weixin.qq.com/connect/qrconnect?appidxxxredirect_urihttps%3A%2F%2Fapi.example.com%2Flogin%2Fcallbackresponse_typecodescopesnsapi_loginstatexxx#wechat_redirect如果redirect_uri部分还出现了%252F这种双重编码那基本就是二次编码问题。4.2 回调接口的域名校验不只是“填域名”开放平台后台让你填“授权回调域”这里填的是域名不需要带https://也不能带路径但实际请求的redirect_uri要注意域名必须完全一致包括端口号。如果你本地调试用的localhost:8080微信回调也会打到http://localhost:8080但这通常不会通过校验因为开放平台默认只允许 80 和 443 端口。所以本地调试时建议先用内网穿透把本地服务映射到公网域名再将这个域名配到回调地址上否则每次都要部署到测试环境才能联调效率极低。4.3 利用的安卓/iOS差异手机扫码后一定要选“在浏览器中打开”有时候用户用微信扫 PC 上的二维码如果在微信内直接点“确认登录”PC 端回调能正常触发但如果用户在微信里点右上角用系统浏览器打开二维码链接部分 Android 厂商浏览器会拦截跳转。这种问题前端无法彻底解决只能在页面提醒用户“请使用微信扫码并在弹出的页面中确认”同时把错误页面做得友好一些。4.4 scope 到底选哪个snsapi_login 才是扫码登录的正确 scope微信扫码登录的scope固定是snsapi_login。和公众号网页授权用的snsapi_base、snsapi_userinfo完全不同。不过扫码登录在拉用户信息时的接口路径和公众号网页授权的用户信息接口是一套/sns/userinfo。很多教程里把这两个概念混在一起讲导致新手以为要选snsapi_userinfo实际上开放平台站点应用只支持snsapi_login这一个值。scope不同还会影响一个行为snsapi_login在用户未关注任何公众号的情况下也能拉到基本信息只是无法拿到unionid和部分敏感信息。如果业务强依赖 unionid建议在开放平台后台完成“绑定公众号”和“绑定开放平台账号”操作否则拿不到。4.5 微信返回“当前网页无法打开”的排查思路这个问题常见于用 iframe 嵌套二维码时报错页面出现在 iframe 内部用户截图发过来才看得到。原因通常是微信对 X-Frame-Options 头限制或者网络问题。排查时先看后端回调是否收到请求如果后端完全没收到可以先用浏览器直接打开 qrUrl 走一遍把问题定位在微信侧还是业务侧。如果确认是 iframe 限制导致的就要评估是否换用window.open或location.href方案。微信官方文档对 iframe 嵌套也是不推荐的但我测试过目前大部分主流浏览器Chrome、Edge、Firefox下 iframe 展示是正常的只有个别安全策略严格的浏览器会拦截。所以如果你面向大众用户建议直接跳转如果面向企业内部iframe 体验更顺滑。5. 实操心得与后续扩展建议微信扫码登录这个功能代码量其实不大真正花时间的是理解授权链路和踩坑排错。这里分享几个我自己的体会扫码登录前一定要先设计好“回调地址怎么从前端路由传递到后端接口”。我见过不少项目把回调地址直接指向前端页面然后前端拿到 code 再传给后端这种流程虽然能做通但存在两个隐患一是 code 暴露在浏览器 URL 中如果前端页面有第三方统计脚本code 可能被收集二是后端无法第一时间拿到 code无法阻止暴力重放。所以后端直接处理回调是更稳妥的设计。用户信息缓存这块微信接口有调用频率限制频繁拉取用户信息会被限流。如果同一个用户短时间内多次登录建议在后端把用户的基本信息缓存到 Redis设置 10 分钟过期减少对微信接口的压力。我实测过缓存后经过高峰时段接口调用量下降了约 80%响应速度也快了不少。后续如果要做“扫码后自动关注公众号”这类需求需要额外引入公众号网页授权的流程但开放平台的扫码登录本身不支持只能通过客服消息或模板消息触达提前规划好用户授权关系。最后再分享一个小技巧联调阶段建议在微信开放平台后台把“应用图标”和“应用名称”都配置完整否则扫码页展示出来的名称很丑会降低用户的信任感。尤其是面向 C 端的项目扫码授权的第一印象会直接影响转化率这个地方值得花十分钟认真配置。
返回列表