ARTICLE DETAIL

资讯详情

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

微信JS-SDK扫一扫实战:从签名配置到scanQRCode调用

微信JS-SDK扫一扫实战:从签名配置到scanQRCode调用 之前在群里碰到个挺典型的提问产品说“前端做个扫一扫功能把二维码扫出来就行”。开发一听第一反应是打开摄像头、接二维码识别库。但真放到微信生态里事情完全不是这么回事——在微信内置浏览器里你既不能随便调用摄像头权限也没法保证普通二维码识别库的精度。真正的“微信扫一扫”指的是通过微信官方JS-SDK提供的wx.scanQRCode接口把微信原生扫一扫界面直接调起来。这篇文章就把这个流程从头到尾拆一遍从账号资质、域名配置到前后端签名链路的每一步运算再到实际调用和常见坑。不管你是第一次接这个能力还是之前被签名问题折磨过按这个思路走一遍基本能通。1. 网页里的“扫一扫”和微信自带扫一扫是两码事1.1 为什么不能直接调摄像头做扫码先纠正一个常见误区H5页面里“扫码”不等于“打开摄像头识别”。浏览器确实提供了getUserMedia接口配合jsQR、zxing-js这类二维码识别库理论上可以在网页里实现扫码。但你真在微信里试一把就会发现问题很多微信内置浏览器iOS是WKWebViewAndroid是X5内核对getUserMedia的支持和权限策略不统一有些版本直接拿不到摄像头流。识别库对光线、遮挡、二维码畸变敏感暗光环境下识别率惨不忍睹。摄像头权限弹窗是浏览器层面的不是微信原生的用户很容易产生疑虑体验非常割裂。即便识别出二维码还要自己处理结果格式文本、URL、Wi-Fi、联系人等规则写不完整就翻车。也就是说纯前端方案的体验、兼容性、识别率三重短板在生产环境里很难让人放心。做演示Demo玩玩可以真上线做业务大概率被测试和用户打回来。1.2 微信扫一扫的本质JS-SDK的能力之一微信提供的扫一扫接口本质不是“拍照识别”而是直接调用微信原生扫一扫界面。也就是说用户点按钮后屏幕上出现的是微信App自带的那个扫码框识别的速度、畸变校正、暗光补光都是微信自身的能力这一层的体验和稳定性接近满分。网页端要做的事情只有一个通过JS-SDK把原生扫一扫叫起来然后在回调里拿结果。从接口归属上看wx.scanQRCode属于微信JS-SDK的“设备信息/能力接口”那一类和wx.getLocation、wx.chooseImage类似都是前端页面借助微信客户端能力来完成的。前置条件也一样必须先经过wx.config完成签名校验让微信确认“这个网页有权限调用这些原生能力”。所以整条链路的重点其实分两块怎么让微信信任你的页面签名和怎么正确调用并处理结果前端逻辑。很多人只盯着第二个问题结果卡死在第一个问题上一整天非常典型。2. 上线前的硬性条件公众号认证与域名配置2.1 账号资质不是随便拿个公众号就能调这是最容易被忽略、但最先卡人的环节。JS-SDK的扫一扫能力前提是你的公众号已经通过微信认证。按实际经验最好直接使用已完成微信认证的服务号别在认证订阅号上赌运气。我在项目里遇到过认证订阅号后台能配上JS安全域名但真机调用时wx.config校验失败、扫码弹不起来的情况换到服务号环境后一切正常。虽然微信官方文档的表述比较模糊但开发阶段如果碰到这种“配置全对就是不行”的诡异问题先怀疑账号类型是值得的。如果只是开发联调可以用微信公众平台的“接口测试号”申请速度很快功能也基本都有适合先跑通流程。但测试号不能用于生产环境上线前还是要换正式账号并且重新走一遍签名配置。另外如果你是在企业微信环境里做扫码还需要区分是自建应用还是第三方应用。自建应用走wx.config第三方应用通常需要走wx.agentConfig两者签名流程和配置入口不一样别拿公众平台的配置经验直接套。2.2 JS接口安全域名配置了不代表配置对了拿到有权限的公众号之后下一步是登录微信公众平台在“设置与开发 - 公众号设置 - 功能设置”中找到“JS接口安全域名”填入你实际部署页面的域名。几个细节非常容易踩坑填域名时不要带协议https://不行也不要带端口号就是裸域名比如example.com或shop.example.com。每个域名需要下载官方校验文件MP_verify_xxxx.txt放到该域名的根目录下并确保通过HTTP/HTTPS都能直接访问到。校验文件上传后不要急着删后续微信会不定时复查。填完保存后微信说有缓存但实际上通常很快生效。如果测试时还是签名失败先确认是不是在缓存期内一般几分钟到几十分钟都有大改域名后最好稍等片刻再测。这里有个容易混淆的地方“JS接口安全域名”和“网页授权域名”是两个完全不同的配置。JS域名管的是wx.config签名和原生能力调用网页授权域名管的是OAuth2静默授权、拿用户openId。扫码功能本身只需要配好JS接口安全域名但如果业务还需要拿用户身份两个都要配别漏。2.3 IP白名单后端服务这边也要配很多人配完前端域名就以为完事了结果后端调接口时报40164提示“此IP地址不在白名单中”。原因很简单获取access_token的接口要求调用方服务器出口IP在公众号后台的“IP白名单”里。具体位置在公众平台“设置与开发 - 安全中心 - IP白名单”。有几类环境容易出问题本地开发时本机访问外网用的公网IP和公司出口IP可能不同要都加进去。服务器走Nginx代理时填的应该是最终访问微信接口的那台机器的出口IP不是Nginx所在内网IP。有些云服务器出口IP和负载均衡的IP还不一致以实际日志里40164报错提示的IP为准。IP白名单不是即时生效一般添加后几分钟内生效测试时不要一报错就立刻重试可能白名单还没同步。3. 后端签名链路wx.config背后的四步运算3.1 签名链路总览前端页面在调用wx.scanQRCode之前必须先执行wx.config传入四个关键参数参数含义appId公众号的唯一标识timestamp生成签名的时间戳秒nonceStr随机字符串防重放signature通过jsapi_ticket、timestamp、nonceStr、url计算出的签名这四个参数不是前端自己随便造的必须由后端生成。完整的获取链路是appId appSecret - access_token - jsapi_ticket - 拼接签名串 - sha1 - signature每一步都有缓存策略和有效期理解这条链路后面排查问题才有方向。3.2 access_token与jsapi_ticket的获取与缓存第一步后端用GET请求微信接口换取access_tokenhttps://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretAPPSECRET返回的JSON里包含access_token和有效期expires_in默认7200秒。这里有个硬性注意点appSecret只能放在后端前端任何地方都不能出现这个值。同时access_token接口的每日调用次数有限制生产环境必须缓存不能每次页面刷新都调一次。第二步拿access_token换jsapi_tickethttps://api.weixin.qq.com/cgi-bin/ticket/getticket?access_tokenACCESS_TOKENtypejsapijsapi_ticket的有效期也是7200秒同样需要缓存。网上有很多文章只强调access_token要缓存但实测下来jsapi_ticket不缓存的话高频调用下更早触发频率限制因为每次签名都要用到它。缓存的工程实现上建议用Redis存这两个值key分开存过期时间设置在7000秒左右留200秒余量避免恰好到期时有请求打过来。高并发场景下要注意缓存穿透问题获取时加个分布式锁同一时刻只允许一个请求去微信拉取其他请求等锁后取缓存。这不是过度设计我见过一个中高流量页面因为没加锁缓存过期瞬间上百个请求同时打到微信接口直接把当天额度刷掉一大半。3.3 签名串构造规则与后端实现拿到jsapi_ticket之后签名就算正式开始了。需要参与签名的字段是下面四个noncestr: 随机字符串不能跟之前重复jsapi_ticket: 就是从上面接口拿到的那一串timestamp: 当前时间戳秒url:当前网页的完整URL注意要去掉#及其后面的部分但?和query参数必须保留接下来按规则构造签名串把这四个字段按字段名的ASCII码从小到大排序。按keyvaluekeyvalue的URL键值对格式拼接成一个字符串。对这个字符串做SHA1哈希得到40位的signature。听起来不复杂但实际操作中url是最大的坑。前端传给后端的URL必须和浏览器里location.href去掉#后的字符串逐字符一致。下面给一个Node.js后端的参考实现包含缓存和签名逻辑const crypto require(crypto); const redis require(./redis-client); // 假设已有redis客户端 const APPID your-app-id; const APPSECRET your-app-secret; async function getAccessToken() { const cacheKey wechat:access_token; const cached await redis.get(cacheKey); if (cached) return cached; const url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid${APPID}secret${APPSECRET}; const res await fetch(url); const data await res.json(); if (data.errcode) { throw new Error(getAccessToken failed: ${data.errcode} ${data.errmsg}); } await redis.set(cacheKey, data.access_token, EX, 7000); return data.access_token; } async function getJsapiTicket() { const cacheKey wechat:jsapi_ticket; const cached await redis.get(cacheKey); if (cached) return cached; const accessToken await getAccessToken(); const url https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token${accessToken}typejsapi; const res await fetch(url); const data await res.json(); if (data.errcode ! 0) { throw new Error(getJsapiTicket failed: ${data.errcode} ${data.errmsg}); } await redis.set(cacheKey, data.ticket, EX, 7000); return data.ticket; } // 这个url由前端传上来必须是当前页面location.href.split(#)[0] function createSignature(ticket, noncestr, timestamp, url) { const params { jsapi_ticket: ticket, noncestr, timestamp, url, }; // 1. 字段名ASCII排序 // 2. 拼接成 keyvaluekeyvalue const string1 Object.keys(params) .sort() .map((key) ${key}${params[key]}) .join(); // 3. sha1签名 return crypto.createHash(sha1).update(string1).digest(hex); } exports.getJsConfig async function (req, res) { const url req.query.url; if (!url || !url.startsWith(http)) { return res.status(400).json({ code: 400, message: invalid url }); } const ticket await getJsapiTicket(); const timestamp Math.floor(Date.now() / 1000); const nonceStr Math.random().toString(36).substring(2, 15); const signature createSignature(ticket, nonceStr, timestamp, url); res.json({ code: 0, data: { appId: APPID, timestamp, nonceStr, signature, }, }); };后端生成好之后暴露一个接口给前端由前端把当前页面url传进来。这里强调一下不要在后端自己拼URL因为经过反向代理、改写地址、大小写变化之后后端拿到的东西和浏览器实际地址很可能不一致签名十有八九会失败。直接用前端上报的location.href.split(#)[0]最稳妥。如果签名调试半天不知道对不对可以先在浏览器里打开微信官方提供的“JS-SDK签名校验工具”页面把你生成的签名参数填进去校验一遍如果工具校验通过说明签名算法没问题问题多半出在URL匹配或配置上。4. 前端调用scanQRCode完整代码与兼容写法4.1 JS文件引入与wx.config注入页面里需要引入微信官方JS-SDK文件script srchttps://res.wx.qq.com/open/js/jweixin-1.6.0.js/script这个JS文件要放在页面里尽早加载但如果页面较复杂也可以选择在需要时动态加载只要保证调用wx.scanQRCode时wx对象已经存在就行。页面加载完成后请求后端签名接口拿到appId、timestamp、nonceStr、signature然后配置wx.config({ debug: false, // 上线前必须关闭否则会弹出大量校验信息 appId: config.appId, timestamp: config.timestamp, nonceStr: config.nonceStr, signature: config.signature, jsApiList: [scanQRCode], // 这里声明需要用到的接口不要多写 });配置成功后微信会触发wx.ready回调配置失败会触发wx.error回调回调里带错误信息。调试阶段强烈建议把debug: true打开能看到“config:ok”或具体的报错原因一目了然。但上线前一定记得关掉否则用户每次打开页面都会弹出一堆签名校验成功的提示体验直接废掉。4.2 调起扫码与结果处理当用户点击页面上的“扫一扫”按钮时调用wx.scanQRCodedocument.getElementById(scanBtn).addEventListener(click, function () { wx.scanQRCode({ needResult: 1, // 1表示需要返回扫码结果0表示不返回 scanType: [qrCode, barCode], // 可以只保留二维码也可以支持条形码 success: function (res) { const result res.resultStr; if (result) { // 拿到结果后做业务处理 handleScanResult(result.trim()); } else { // needResult为0时这里拿不到结果 alert(未获取到扫码结果); } }, cancel: function () { // 用户中途取消扫码 console.log(用户取消了扫码); }, fail: function (err) { // 调用失败比如没有权限或未正确注入 console.error(scanQRCode fail, err); alert(扫一扫调用失败请确认是否在微信内打开); }, }); });needResult这个参数要特别说清楚设为1时扫码成功后微信会把结果字符串通过success回调的res.resultStr返回给页面设为0时微信扫码完成后不会把结果回传给前端。项目里如果只是想让用户“扫一下码”然后自己处理比如扫门店二维码加关注可以设0但绝大多数业务场景扫设备码、扫订单码、扫商品码都需要needResult: 1。success回调里拿到的res.resultStr可能是URL、纯文本、数字或JSON字符串取决于二维码内容。做业务处理时最好先判断类型再走分支比如以http开头走跳转逻辑纯数字走设备绑定逻辑避免一刀切处理。4.3 复杂场景SPA路由变化后的二次签名如果你用的是Vue、React这类SPA框架并且路由模式是HTML5 History模式这里有一个非常隐蔽的坑签名是和URL绑定的。wx.config虽然只执行一次但SPA页面在路由跳转后URL已经变了而微信校验的是签名时对应的那个URL。也就是说A页面签的名在B页面就无效了。解决办法是监听路由变化在跳转后重新请求后端签名接口带上新URL再wx.config一次。如果是Hash模式的路由URL里有#情况会简单一些因为签名时不包含#后面的部分签名串里的url始终是同一个。但这也意味着你不能在签名后共享同一个wx.config去调用需要不同URL的权限判断hash变化时微信通常不会重新校验但实际项目里还是建议在页面级别的操作前后主动校验一下。另外一个容易忽略的点如果在iframe里嵌入了页面微信的JS-SDK签名校验是以iframe内页面的实际URL为准的不是外层页面URL。跨域iframe下签名配置会非常麻烦建议能不用就不用。4.4 非微信浏览器环境下的降级处理wx.scanQRCode只在微信内置浏览器或企业微信内置浏览器里可用用户在普通浏览器Chrome、Safari里打开页面时wx对象不存在或wx.scanQRCode没有定义。前端在调用前最好做一次环境判断function isWeChatBrowser() { const ua navigator.userAgent.toLowerCase(); return ua.indexOf(micromessenger) ! -1; }不是微信环境时可以引导用户“请在微信中打开”或者降级为使用getUserMedia加二维码识别库的方案如果你的业务确实需要支持外部浏览器。但要注意降级方案的识别率和权限问题都远不如原生扫码生产环境慎重。5. 实战踩坑记录从签名失败到扫码框不弹出5.1 invalid signature的排查链路wx.error里报invalid signature或config:invalid signature是整个流程中出现频率最高的错误。别慌按下面链路一步步查基本能定位先确认URL完全一致。在wx.error回调里打印location.href在后端日志里打印收到并参与签名的url逐字符对比。重点看有没有多一个/、大小写、协议https还是http、有没有带上#后的内容、有没有被前端encodeURIComponent编过。实测中前端传来encodeURIComponent(location.href)导致后端解码后与签名时URL不一致的情况几乎每周都能遇到一次。确认signature不是拿旧的jsapi_ticket生成的。jsapi_ticket缓存过期后如果后端没有重新拉取还继续用旧值签名必然失败。排查时可以在后端打印生成签名时用的jsapi_ticket前20位跟微信公共接口刚拉到的ticket前20位对比。确认appId与ticket是否匹配。如果项目里有多个公众号后端拿A公众号的ticket去给B公众号的页面签名结果一定是invalid signature这种错误不仔细看日志根本发现不了。测试号与正式号混用。开发环境用测试号、生产用正式号如果前端页面某个地方把正式号的appId和测试号后端生成的signature拼在一起也会挂。结合微信官方的签名校验工具基本能把签名算法问题排除掉。剩下的就是环境配置和URL匹配问题。5.2 安卓和iOS的差异微信扫一扫在安卓和iOS上的表现不是完全一致的以下几个点实测遇到过iOS对URL大小写敏感后端做URL比对时如果做了toLowerCase()iOS上很容易导致签名失败。iOS的location.href有时候会保留路径大小写而安卓某些内核会自动把小写化签名时要以前端实际location.href为准不要做任何改写。某些安卓手机在扫码成功后紧跟一个location.href跳转会丢失回调。也就是说在success里拿结果后立刻跳转页面上还没来得及处理业务逻辑就卸载了。稳妥做法是先把resultStr保存到全局变量或sessionStorage再在setTimeout里跳转或者先做业务请求成功后再跳转。结果字符串可能带空格或不可见字符。二维码生成工具千奇百怪有些会在内容前后加换行符或BOM头直接拿去请求后端接口容易莫名报错。建议拿到结果统一trim()必要时再按\n分割处理。5.3 扫码框弹不出的另类原因如果签名校验已经通过wx.ready正常触发但点按钮后扫码框就是不弹排查方向要转向下面几个点页面不是微信内置浏览器打开。这个常见于开发调试时在PC浏览器里联调扫码肯定弹不出来的。真机必须用微信扫一扫打开页面。jsApiList里没声明scanQRCode。有人只写wx.config配置基本参数但jsApiList是空的结果调用时微信直接报“no permission”。scanQRCode必须出现在这个数组里。调用的按钮事件绑定时机不对。如果页面是异步渲染的事件绑定在DOM元素创建之前点击时函数根本没绑上看起来就像“扫码框没弹”。这个问题跟微信SDK无关但很容易被误判。页面存在多层iframe嵌套。微信对iframe中的JS-SDK调用支持非常有限特别是跨域iframe扫码框可能直接不出现。解决办法是把扫码入口放到顶层页面或者用postMessage把扫码指令传给顶层页面执行。如果你已经上了生产环境debug: false看不到日志建议在扫码按钮回调里临时挂一个vConsole或者把wx.error的内容上报到自己的监控系统。扫码链路出问题时有日志和没日志排查时间能差出好几倍。写在最后的几点个人经验项目做多了我自己总结了一套比较稳的接入节奏先用测试号跑通全链路确认签名和扫码都没问题再切正式号重新配置JS安全域名和IP白名单上线前关掉debug在wx.error和scanQRCode.fail里做好埋点。这套顺序看起来很基础但能帮你把“配置问题”和“代码问题”分开定位省掉大量无效排查时间。另外有个小技巧签名用的url不要前端传什么就信什么后端可以加一层校验对格式、协议、域名做透传前的白名单匹配。这样即使有人恶意传一个不在你业务域名下的URL也能直接拦截别让签名接口变成给别人页面授权的“公共工具”。还有扫码拿到的结果如果是一个URL不要在前端直接location.href跳转。原因很简单二维码是公开的内容可以被任何人伪造直接跳转存在被诱导到钓鱼页面的风险。稳妥做法是先把结果提交到后端由后端做安全校验是否是业务域名、是否在允许列表内后再决定是否跳转。扫码是入口安全校验永远不能在入口处省掉。
返回列表