- 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。
08-06 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - 【物流助手】服务商如何接入“即时配送”?
参考下公告: https://developers.weixin.qq.com/community/minihome/doc/000c8aa962c0c0f24ceb574d55d801
2021-12-30 - 申请开通即时配送权限?
很急,麻烦开通下即时配送
2021-02-25 - 平台型服务商代实现企业官网小程序(服务器版)
一、帐号准备1 第三方平台类型选择:平台型服务商 2 权限选择:开发管理与数据分析权限 3 开发配置1)登录授权的发起页域名:w.cloudbase.vip 2)授权事件接收URL:w.cloudbase.vip/call 注意:在这里只是教程演示的URL,请根据自己的域名设置 请根据以上内容在微信开放平台注册一个第三方平台帐号,具体流程可以参照第一章节的步骤。 二、基础信息设置1 开发环境搭建1.1 准备服务器环境,设定域名请准备一个线上服务器环境,并可以在公网访问到。而且服务器的域名必须为授权事件接收URL的域名。 在本教程中,以NodeJS服务器环境为例子,设定域名为w.cloudbase.vip 同时将服务器的IP地址填写到第三方平台的白名单中,修改保存 [图片] 1.2 下载部署服务器DEMO示例代码为了能够更加清楚的理解之后的步骤,请下载服务器node示例,并按照示例中的部署步骤部署到准备的服务器环境中。 打开项目的work/key.json,将以下第三方平台基础的开发信息写入文件: { "appId": "第三方平台APPID", "component_appid": "第三方平台APPID(同上)", "component_appsecret": "第三方平台appsecret", "encodingAESKey": "加解密Key", "token": "随机字符", "template_id":"小程序模板id,先空,后续步骤添加", "redirect_uri":"授权发起页url,需为此项目部署的域名,而且在开放平台设置,如此项目可以通过www.example.com访问到index页,则填写http://www.example.com" } 2 创建授权事件接收URL的服务监听请定位到项目work/call,在这里执行了一个事件接收监听逻辑,此目录的代码模块在router中以/call的形式外发,这样接口URL地址为w.cloudbase.vip/call,正是在微信平台中设置的授权事件接收URL。 我们提取出关键代码,此模块的入口文件代码如下: const db = require('../database/lowdb');//使用文件数据库LowDB封装的数据存取接口 function main(msg_body,query){ //从event中可以获取到HTTP请求的有关信息 //event.body即为请求体 console.log(msg_body,query); /*event.queryStringParameters中可以获得请求参数,取出以下三个内容 - timestamp 时间戳 - nonce 随机数 - msg_signature 消息体签名,用于验证消息体的正确 */ let { msg_signature, nonce, timestamp } = query; //判断签名是否不为空,过滤一些非开放平台的无效请求 if (msg_signature != null) { //针对信息进行base64解码 let encryptedMsg = Buffer.from(msg_body, 'base64').toString(); //取出加密的encrypt,在这里没有使用XML方式读取 let encrypt = encryptedMsg.slice(encryptedMsg.indexOf(') + 18, encryptedMsg.indexOf(']]>')); //引入util.js文件,命名为WechatEncrypt,此js包含解码的所有逻辑 const WechatEncrypt = require('./util'); //引入key.json,在这里存储了第三方平台设置的key,随机码,appid等 const WXKEY = require('../key.json'); //将key.json的内容代入,创建WechatEncrypt实例 const wechatEncrypt = new WechatEncrypt(WXKEY); //将timestamp 时间戳、nonce 随机数、加密的encrypt代入gensign函数进行签名处理 let signature = wechatEncrypt.genSign({ timestamp, nonce, encrypt }); //判断签名是否和传来的参数签名一致 if (signature === msg_signature) { //将加密的encrypt直接代入decode函数进行解码,返回解码后的明文 let xml = wechatEncrypt.decode(encrypt); //判断明文中是否有ComponentVerifyTicket字段,由此来判断此为验证票据 if (xml.indexOf('ComponentVerifyTicket') != -1) { //取出相应的票据,在这里没有使用XML方式读取 let ticket = xml.slice(xml.indexOf('ticket@@@'), xml.indexOf(']]>')); try { //将票据信息保存到wxid数据库中,component_verify_ticket文档中 console.log(ticket,0); db.update_cvt(ticket,0); } catch (e) { console.log('save failed!', e); } } return 'success'; } else { return 'error'; } } else { return 404; } } module.exports = main; 在index.js同级目录下,有util.js,此为解码的主要逻辑文件,编写代码如下: const crypto = require('crypto') const ALGORITHM = 'aes-256-cbc' // 使用的加密算法 const MSG_LENGTH_SIZE = 4 // 存放消息体尺寸的空间大小。单位:字节 const RANDOM_BYTES_SIZE = 16 // 随机数据的大小。单位:字节 const BLOCK_SIZE = 32 // 分块尺寸。单位:字节 let data = { appId: '', // 微信公众号 APPID token: '', // 消息校验 token key: '', // 加密密钥 iv: '' // 初始化向量 } const Encrypt = function (params) { let { appId, encodingAESKey, token } = params let key = Buffer.from(encodingAESKey + '=', 'base64') // 解码密钥 let iv = key.slice(0, 16) // 初始化向量为密钥的前16字节 Object.assign(data, { appId, token, key, iv }) } Encrypt.prototype = { /** * 加密消息 * @param {string} msg 待加密的消息体 */ encode(msg) { let { appId, key, iv } = data let randomBytes = crypto.randomBytes(RANDOM_BYTES_SIZE) // 生成指定大小的随机数据 let msgLenBuf = Buffer.alloc(MSG_LENGTH_SIZE) // 申请指定大小的空间,存放消息体的大小 let offset = 0 // 写入的偏移值 msgLenBuf.writeUInt32BE(Buffer.byteLength(msg), offset) // 按大端序(网络字节序)写入消息体的大小 let msgBuf = Buffer.from(msg) // 将消息体转成 buffer let appIdBuf = Buffer.from(appId) // 将 APPID 转成 buffer let totalBuf = Buffer.concat([randomBytes, msgLenBuf, msgBuf, appIdBuf]) // 将16字节的随机数据、4字节的消息体大小、若干字节的消息体、若干字节的APPID拼接起来 let cipher = crypto.createCipheriv(ALGORITHM, key, iv) // 创建加密器实例 cipher.setAutoPadding(false) // 禁用默认的数据填充方式 totalBuf = this.PKCS7Encode(totalBuf) // 使用自定义的数据填充方式 let encryptdBuf = Buffer.concat([cipher.update(totalBuf), cipher.final()]) // 加密后的数据 return encryptdBuf.toString('base64') // 返回加密数据的 base64 编码结果 }, /** * 解密消息 * @param {string} encryptdMsg 待解密的消息体 */ decode(encryptdMsg) { let { key, iv } = data let encryptedMsgBuf = Buffer.from(encryptdMsg, 'base64') // 将 base64 编码的数据转成 buffer let decipher = crypto.createDecipheriv(ALGORITHM, key, iv) // 创建解密器实例 decipher.setAutoPadding(false) // 禁用默认的数据填充方式 let decryptdBuf = Buffer.concat([decipher.update(encryptedMsgBuf), decipher.final()]) // 解密后的数据 decryptdBuf = this.PKCS7Decode(decryptdBuf) // 去除填充的数据 let msgSize = decryptdBuf.readUInt32BE(RANDOM_BYTES_SIZE) // 根据指定偏移值,从 buffer 中读取消息体的大小,单位:字节 let msgBufStartPos = RANDOM_BYTES_SIZE + MSG_LENGTH_SIZE // 消息体的起始位置 let msgBufEndPos = msgBufStartPos + msgSize // 消息体的结束位置 let msgBuf = decryptdBuf.slice(msgBufStartPos, msgBufEndPos) // 从 buffer 中提取消息体 return msgBuf.toString() // 将消息体转成字符串,并返回数据 }, /** * 生成签名 * @param {Object} params 待签名的参数 */ genSign(params) { let { token } = data let { timestamp, nonce, encrypt } = params; let rawStr = [token,timestamp,nonce,encrypt].sort().join('') // 原始字符串 let signature = crypto.createHash('sha1').update(rawStr).digest('hex') // 计算签名 return signature }, /** * 按 PKCS#7 的方式从填充过的数据中提取原数据 * @param {Buffer} buf 待处理的数据 */ PKCS7Decode(buf) { let padSize = buf[buf.length - 1] // 最后1字节记录着填充的数据大小 return buf.slice(0, buf.length - padSize) // 提取原数据 }, /** * 按 PKCS#7 的方式填充数据结尾 * @param {Buffer} buf 待填充的数据 */ PKCS7Encode(buf) { let padSize = BLOCK_SIZE - (buf.length % BLOCK_SIZE) // 计算填充的大小。 let fillByte = padSize // 填充的字节数据为填充的大小 let padBuf = Buffer.alloc(padSize, fillByte) // 分配指定大小的空间,并填充数据 return Buffer.concat([buf, padBuf]) // 拼接原数据和填充的数据 } } module.exports = Encrypt 我们通过router将此模块进行接口外发,代码如下: router.post("/call", function (req, res, next) { res.send(require('../work/call/index')(req.body,req.query)); }); 于是,我们可以通过wx.cloudbase.vip/call这个地址来接收请求。微信开放平台将每隔10分钟左右就向此url发送请求(因为我们在第三方平台创建时填写的此url),此接口便可以完成请求的解析和解密存储操作。 温馨提示:以js来演示消息解密过程,其中util文件中仍然包含加密函数,有需要的同学可以自行研究使用,util无需修改直接可用,如果你想使用其他语言版本,请在官方文档中下载代码示例 3 使用接收到的验证票据(component_verify_ticket)获取令牌请定位到项目work/getComToken文件夹,在这里封装了通过ticket获取令牌的相关逻辑,入口代码如下: const request = require('request'); const db = require('../database/lowdb');//使用文件数据库LowDB封装的数据存取接口 //获取相关的第三方平台信息 const { component_appid, component_appsecret } = require('../key.json'); //封装的http请求函数 function CallWeb(ticket) { return new Promise((resolve, reject) => { request({ url: 'https://api.weixin.qq.com/cgi-bin/component/api_component_token',//请求的API地址 body: JSON.stringify({ component_appid, component_appsecret, component_verify_ticket: ticket }),//传递的所需参数 method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } async function main(event){ try { //由于令牌有一定时效性,所以我们没必要每一次都要请求,而是将令牌保存重复利用,我们将令牌保存在wxid数据库中的component_access_token文档里 //首先取出文档的信息 let access_token = db.get_cat(); //以当前时间的往后一分钟来作为上限时间 let overtime = new Date((new Date()).valueOf() + 60 * 1000); //如果文档的令牌超时时间大于上限时间,则证明令牌还有效,直接返回令牌 if (access_token.time > overtime) { return access_token.value; } else { //如果小于则证明令牌过期,需要重新申请 console.log('token timeover!'); try { //取出ticket票据信息 let ticket = db.get_cvt(); //将票据信息传入http请求函数,等待请求结果 let result = await CallWeb(ticket.value); //结果是一个json字符串,验证是否有component_access_token字样,如果有则没有报错 if (result.indexOf('component_access_token') != -1) { //解析字符串为json let { component_access_token, expires_in } = JSON.parse(result); try { //更新令牌,并设定超时时间为当前时间的有效时效后,expires_in为有效秒数 db.update_cat(component_access_token,expires_in); //返回新的令牌 return component_access_token; } catch (e) { console.log('access save failed!', e); return null; } } else { console.log('wxcall failed!', result); return result; } } catch (e) { console.log('ticket failed!', e); return null; } } } catch (e) { console.log('access get failed!', e); return null; } } module.exports = main; 如此,当其他模块需要使用第三方平台的access_token,就可以直接用以下方式来调用了,将会返回最新的access_token: //通过调用getComToken接口获取第三方令牌 let access_token = await require('../getComToken/index')(); 三、授权流程配置1 使用令牌获取预授权码并拼接用户授权链接根据官方文档-获取预授权码的描述,我们需要component_access_token(第三方平台令牌)、component_appid(第三方平台appid),API接口为:https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${component_access_token} 在示例项目中定位work/getPreAuth目录,此包含获取预授权码并封装授权链接的逻辑,代码如下: const request = require('request'); //获取相关的第三方平台信息 const { component_appid } = require('../key.json'); const db = require('../database/lowdb');//使用文件数据库LowDB封装的数据存取接口 //封装的http请求函数 function CallWeb(token) { return new Promise((resolve, reject) => { request({ url: 'https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=' + token, body: JSON.stringify({ component_appid }), method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } async function main(event){ //此接口是由后台管理页面请求的,在调用此接口时就意味着想要创建一个授权的对象,name是对这个对象的备注 if(event.name==null||event.name=='') return null; try { //通过调用getComToken接口获取第三方令牌 let access_token = await require('../getComToken/index')(); if (access_token != null) { //将令牌信息传入http请求函数,等待请求结果 let result = await CallWeb(access_token); //结果是一个json字符串,验证是否有pre_auth_code字样,如果有则没有报错 if (result.indexOf('pre_auth_code') != -1) { //解析字符串为json let { pre_auth_code, expires_in } = JSON.parse(result); //拼接授权链接,根据此文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Authorization_Process_Technical_Description.html let auth_url = `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${component_appid}&pre_auth_code=${pre_auth_code}&redirect_uri=https://wx.cloudbase.vip/cloud&auth_type=2`; //将授权链接保存到mini数据库内 return db.update_mini(null,{ status: 0, name:event.name, url: auth_url, time: new Date(new Date().getTime() + 1000 * expires_in) }) } else { console.log('wxcall failed!', result); return result; } } else { console.log('token get failed'); return null; } } catch (e) { console.log('get failed!', e); return null; } } module.exports = main; 由于所有的模块都遵循work/模块名称/index的路径逻辑,所以在router中可以进行统一的中转外发,如下代码所示: router.post("/webcall", async function (req, res, next) { let {name,data} = req.body; if(name){ res.send(await require(`../work/${name}/index`)(data)); } else{ res.send(404); } }); 我们定位到views/admin.ejs,这是示例项目中简单的管理平台,主要实现了整个的授权小程序管理和部署操作。 其中,在admin.ejs中,我们可以调用getPreAuth模块获取授权路径,如下代码所示: function getPreAuth() { let name = prompt("请输入授权备注名", ""); if (name) { callFunction({ name: 'getPreAuth', data: { name: name } }).then(res => { if (res != null) { console.log('获取成功', res.id); init(); } else { alert('获取授权链接失败!') } }); } } function deletepre(id) { callFunction({ name: 'admindelete', data: { id: id } }).then(res => { init(); }); } function callFunction(obj){ return new Promise((resolve, reject) => { let xml=new XMLHttpRequest(); xml.open("POST","webcall",true) xml.setRequestHeader("Content-type","application/json"); xml.send(JSON.stringify({ name:obj.name, data:obj.data==null?{}:obj.data })); xml.responseType='text'; xml.onreadystatechange=function() { if (xml.readyState === 4 && xml.status === 200) { let e = JSON.parse(xml.responseText); resolve(e); } } }); } admin页面的router外发配置如下: router.get("/admin", function (req, res, next) { res.render("admin", {}); }); 我们可以在页面中通过新增授权来获取一个授权链接了。 2 通过授权发起页域名打开授权链接引导用户扫码当授权链接创建成功后,我们就能够在管理端admin中得到授权的列表了,列表中每一个项都由数据库文档id来唯一标示,当我们在页面中点击链接时,就可以跳转到域名跟路径了。 域名根路径下是index页面,相关路由代码如下: router.get("/", function (req, res, next) { res.render("index", {}); }); 在项目中定位到views/index.ejs文件,在这里是模拟客户的页面。 代码如下: 客户授权title> head> 这是微信第三方平台测试——客户端接口div> let msg_text = document.getElementById('text'); let query = getQueryString(); if (query.do != null) { msg_text.innerText = "拉取授权页面中……" callFunction({ name: 'authGetOne', data: { id: query.do } }).then(res => { console.log(res); if (res) { if (res.status == 0) { window.localStorage.setItem('open_auth_id', query.do); window.location = res.url; } else { msg_text.innerText = "已授权!" } } else { msg_text.innerText = "授权路径不存在!" } }); } function getQueryString() { var qs = location.search.substr(1), args = {}, items = qs.length ? qs.split("&") : [], item = null, len = items.length; for (var i = 0; i < len; i++) { item = items[i].split("="); var name = decodeURIComponent(item[0]), value = decodeURIComponent(item[1]); if (name) { args[name] = value; } } return args; } function callFunction(obj) { return new Promise((resolve, reject) => { let xml = new XMLHttpRequest(); xml.open("POST", "webcall", true) xml.setRequestHeader("Content-type", "application/json"); xml.send(JSON.stringify({ name: obj.name, data: obj.data == null ? {} : obj.data })); xml.responseType = 'text'; xml.onreadystatechange = function () { if (xml.readyState === 4 && xml.status === 200) { let e = JSON.parse(xml.responseText); resolve(e); } } }); } script> body> html> 这个页面在打开的时候,会根据参数想authGetOne接口去获取授权链接,而参数其实就是创建授权链接时保存在数据库中的文档id。 我们定位到work/authGetOne目录,此处实现了通过文档id获取整个授权信息的逻辑。 const db = require('../database/lowdb');//使用LOWDB实现的简易数据库接口。 function main(event){ if(event.id!=null){ //我们在前面步骤已经存入了一个id return db.get_lowmini(event.id); } else{ return null; } } module.exports = main; 我们就可以使用w.cloudbase.vip?do=+(上一步保存的id)这个链接来实现授权页面的跳转了。(w.cloudbase.vip为本教程专用,实际实践需要改写为自己的) 3 获取用户授权信息3.1 根据用户授权后的授权码获取用户授权账号的信息用户授权后,会在回调的页面中回传授权码,具体表现如下: [图片] 其中auth_code参数即为授权码的信息,我们改造一下上一步的index.ejs,在if逻辑下添加else if逻辑 else if (query.auth_code != null) { msg_text.innerText = "校验中……" let temp_id = window.localStorage.getItem('open_auth_id'); if (temp_id != null) { callFunction({ name: 'authUpdateOne', data: { code: query.auth_code, id: temp_id } }).then(res => { console.log(res); if (res != null) { window.localStorage.removeItem('open_auth_id'); console.log(res); msg_text.innerText = "校验成功!"; } else { msg_text.innerText = "校验失败,请刷新此页面重试!" } }); } else { msg_text.innerText = "路径已经刷新,请联系平台!" } } 当用户授权成功后,回调回此网址,并附带参数,我们便可以根据参数来进行令牌的获取了。在上面代码中调用了authUpdateOne接口。 我们定位到work/authUpdateOne目录,此实现了获取用户授权的开发相关信息。代码如下 const request = require('request'); //获取相关的第三方平台信息 const { component_appid } = require('../key.json'); const db = require('../database/lowdb'); //封装的http请求函数 function CallWeb(token,code) { return new Promise((resolve, reject) => { request({ //获取授权信息的API url: 'https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=' + token, body: JSON.stringify({ component_appid, authorization_code:code }), method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } async function main(event){ if(event.code==null||event.id==null) return null; try { //通过调用getComToken接口获取第三方令牌 let access_token = await require('../getComToken/index')(); if (access_token != null) { //将令牌信息和授权码传入http请求函数,等待请求结果 let result = await CallWeb(access_token,event.code); console.log(result); //结果是一个json字符串,验证是否有authorization_info字样,如果有则没有报错 if (result.indexOf('authorization_info') != -1) { //解析字符串为json let { authorization_info } = JSON.parse(result); //取出字段 let { authorizer_access_token,authorizer_appid,authorizer_refresh_token,expires_in,func_info} = authorization_info; //存储到mini中相应的文档里,并置状态为1 return db.update_mini(event.id,{ status:1, time:new Date(), func_info, access_token:authorizer_access_token, access_time:new Date(new Date().getTime() + 1000 * expires_in), appid:authorizer_appid, refresh_token:authorizer_refresh_token }); } else { console.log('wxcall failed!', result); return result; } } else { console.log('token get failed'); return null; } } catch (e) { console.log('get failed!', e); return null; } } module.exports = main; 当获取成功后,就会在页面中提示授权成功的信息,用户授权步骤完成。 以上是在用户的角度来看到的,在第三方平台角度看到的信息如下,我们可以获得授权账户的appid、接口令牌、刷新令牌、授权集信息。 由于授权用户可以自主修改授权的信息,所以我们需要判断用户授权的授权集是否满足我们的业务开发要求,在下图字段func_info我们可以获得权限数据的列表,我们应该有相应的判断,来进行复核确认,官方文档中已经清楚的写明了。 一切确定了之后,我们便可以根据这些来进行后续的开发操作了。 [图片] 3.2 过期的授权令牌如何重新刷新在官方文档中描述,每次提供令牌时,都附带刷新令牌,我们需要通过刷新令牌来去获取最新的令牌,由此反复。 需要注意的是,要好好保存刷新令牌,如果丢失需要用户重新授权才可以。 我们定位到work/getAuthToken目录,此实现了获取刷新令牌的逻辑,代码如下: const request = require('request'); const { component_appid } = require('../key.json'); const db = require('../database/lowdb'); function CallWeb(refresh_token, access_token, auth_appid) { return new Promise((resolve, reject) => { request({ url: 'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=' + access_token, body: JSON.stringify({ component_appid: component_appid, authorizer_appid: auth_appid, authorizer_refresh_token: refresh_token }), method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } async function main (event){ console.log(event); if(event.appid==null)return; try { let auth_data = db.query_mini(event.appid); console.log(auth_data); if (auth_data) { let { access_token, access_time, refresh_token , uuid} = auth_data[0]; let overtime = new Date((new Date()).valueOf() + 60 * 1000); if (access_time > overtime) { return access_token; } else { console.log('token timeover!'); let access_token = await require('../getComToken/index')(); if (access_token != null) { let result = await CallWeb(refresh_token, access_token, event.appid); console.log(result); if (result.indexOf('authorizer_access_token') != -1) { let { authorizer_access_token, authorizer_refresh_token, expires_in } = JSON.parse(result); db.update_mini(uuid,{ access_token: authorizer_access_token, access_time: new Date(new Date().getTime() + 1000 * expires_in), refresh_token: authorizer_refresh_token }); return authorizer_access_token } else { console.log('wxcall failed!', result); return null; } } else { console.log('token get failed'); return null; } } } else { console.log('can not get appid!'); return null; } } catch (e) { console.log('access get failed!', e); return null; } } module.exports = main; 我们可以通过此接口直接获取令牌,如果令牌过期会自动刷新令牌并保存,以备下一次使用。 四、 构建小程序模版 我们在了解完模板小程序的创建过程以及部署流程之后,我们就开始开发编写一个小程序,为了能够较为完整的演示所有可能性,此小程序是一个使用后端服务的企业展示小程序,具体效果如下: [图片] 界面数据直接使用wx.request来获取业务服务API的信息(此处需要自行开发) 具体代码可以点击这里下载,其中关键的逻辑信息如下: const app = getApp() var that = null; Page({ onShow() { //加载本地存储 let data = wx.getStorageSync('data'); //如果没有存储,直接请求云端数据 if(data==null||data==""){ this.init(); } else{ //有数据即直接部署数据 this.setData(data); wx.setNavigationBarTitle({ title: data.name, }) } }, //云端请求数据 init(){ that = this; wx.request({ url: 'https://ex.com/get', //仅为示例,请更换为自己的api链接 header: { 'content-type': 'application/json' }, success (res) { //部署数据,设置标题栏内容 if(res.data.name!=null){ that.setData(res.data); wx.setStorageSync('data', res.data); wx.setNavigationBarTitle({ title: res.data.name, }) } }, fail(e){ wx.showModal({ title:"测试失败", content:JSON.stringify(e) }); } }); }, telcall(){ wx.makePhoneCall({ phoneNumber: this.data.tel, }) } }) 服务器API返回的数据格式如下(需自行在自己服务器实现返回): data:{ content:'企业的描述', logo:"企业的logo照片地址", name:"企业的名称", person:"企业的代表人", tel:"企业的电话" } 在这里特别说明一下,使用传统服务器形式通信的小程序,直接进行request请求调用自定义的url接口(如上边代码的https://ex.com/get),需要注意此处调用的url域名需要在设置的白名单中。 在开发完毕后,即可上传到第三方平台的草稿箱,参照第一章节的步骤将其转化为小程序模版,即可使用模版ID进行下面的部署步骤了。 五、小程序模版部署到小程序中(上传代码与提审)1 小程序模版部署到授权小程序在这里我们将使用第三方平台的API接口来执行部署过程。由于所有第三方API接口都需要在白名单中才可以调用。 我们定位到work/uploadcode目录,这里实现了整个小程序部署上传的逻辑: const request = require('request'); function CallWeb(access_token, data) { return new Promise((resolve, reject) => { request({ url: `https://api.weixin.qq.com/wxa/commit?access_token=${access_token}`, body: JSON.stringify(data), method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } async function main(event){ let { appid,name } = event; if (appid != null && name != null) { let access_token = await require('../getAuthToken/index')({ appid }); if (access_token != null) { let result = await CallWeb(access_token, { template_id : "2", user_version : "1.0.1", user_desc : "第三方平台代开发", ext_json : `{ "extEnable": true, "extAppid": "${appid}", "directCommit": false, "window":{ "navigationBarTitleText": "${name}" }}` }); try{ let res = JSON.parse(result); console.log(res); return res; } catch(e){ console.log(e,result); return result; } } else { return { code: -1, msg: 'access_token is null' } } } else { console.log(event); return 404; } } module.exports = main; 在admin页面中,有调用此接口进行小程序模版上传部署,相关代码如下: function uploadCode(){ showdesw('开始上传小程序代码……'); callFunction({ name: 'uploadCode', data: { appid: input.appid, name:input.name } }).then(res => { console.log(res); showdesw('小程序代码上传成功!'); }).catch(e => { console.log(e); }); } 2 授权小程序服务器域名配置我们需要通过官方文档-设置服务器域名来进行域名的设置,与小程序代码的请求地址保持对应,不要设置错。 关于服务器域名设置这里,需要特别注意的是需要先将域名登记到第三方平台的小程序服务器域名中,才可以调用接口进行配置。而且配置时会自动删除原先配置的域名,需要特别注意 具体代码如下: const request = require('request'); //封装的http请求函数 function CallWeb(access_token,data) { return new Promise((resolve, reject) => { request({ url: `https://api.weixin.qq.com/wxa/modify_domain?access_token=${access_token}`,//拼接相应的name,以及令牌 body: JSON.stringify(data),//请求的数据参数 method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } exports.main = async (event) => { //获取appid,参数 let { appid } = event; if (appid != null && name != null && data != null) { //传入appid,获得相应的操作令牌 let access_token = await require('../getAuthToken/index')({ appid }); if (access_token != null) { //传入参数,等待数据 let result = await CallWeb(access_token,{ action:"add", requestdomain: ["https://www.qq.com", "https://www.qq.com"] }); try{ let res = JSON.parse(result); console.log(res); return res; } catch(e){ console.log(e,result); return result; } } else { return { code: -1, msg: 'access_token is null' } } } else { console.log(event); return 404; } } 3 授权小程序提交审核当小程序模版上传、服务器配置完毕之后,就需要提交审核了。 相关代码如下: const request = require('request'); //封装的http请求函数 function CallWeb(access_token,data) { return new Promise((resolve, reject) => { request({ url: `https://api.weixin.qq.com/wxa/submit_audit?access_token=${access_token}`,//拼接相应的name,以及令牌 body: JSON.stringify(data),//请求的数据参数 method: 'POST' }, (error, response, body) => { if (error) { reject(error); } resolve(response.body); }); }); } exports.main = async (event) => { //获取appid,参数 let { appid } = event; if (appid != null && name != null && data != null) { //传入appid,获得相应的操作令牌 let access_token = await require('../getAuthToken/index')({ appid }); if (access_token != null) { //传入参数,等待数据 let result = await CallWeb(access_token,{ version_desc:"这是我的小程序,请审核" }); try{ let res = JSON.parse(result); console.log(res); return res; } catch(e){ console.log(e,result); return result; } } else { return { code: -1, msg: 'access_token is null' } } } else { console.log(event); return 404; } } 代码示例本教程用到的示例代码如下: 全代码示例-服务器node版本
2021-09-09 - 微信开放平台第三方代实现小程序业务简述
一、微信公众平台-第三方平台开发(什么是第三方平台) 微信公众平台-第三方平台(简称第三方平台)开放给所有通过开发者资质认证后的开发者使用。在得到公众号或小程序运营者(简称运营者)授权后,第三方平台开发者可以通过调用微信开放平台的接口能力,为公众号或小程序的运营者提供账号申请、小程序创建、技术开发、行业方案、活动营销、插件能力等全方位服务。同一个账号的运营者可以选择多家适合自己的第三方为其提供产品能力或委托运营。 从具体的业务场景上说,第三方平台包括以下场景: 1、提供行业解决方案,整体打包公众号或小程序的产品开发等; 2、行业:提供更加专业的运营能力,精细化运营用户公众号或小程序; 3、功能:对公众平台功能的优化,如专门优化图文消息视觉样式和排版的工具,或专门定制的 CRM 用户管理功能,或功能强大的小程序插件等。 二、第三方平台代实现小程序业务 第三方平台代实现小程序业务,即第三方平台帮助旗下已授权的小程序进行代码管理。和普通的小程序开发流程相比,第三方代实现小程序业务,需要先开发完成小程序模板,再将小程序模板部署到旗下小程序帐号中 三、从0到1代实现小程序业务 3.1 微信开放平台注册与认证 在微信开放平台https://open.weixin.qq.com/ 注册帐号,并在账号中心中,完成认证。 [图片] 3.2 创建第三方平台 1)在“微信开放平台-管理中心-第三方平台”创建第三方平台。 [图片] 2)选择对应的服务商类型进行第三方平台创建。 *定制化开发服务商:指具备完整开发独立小程序或插件,并可以提供后续运营的服务商。定制化服务商,可将自己已经开发出的定制化小程序关联到服务商平台中,生成凭证(票据)填充到小程序代码包中进行关联。平台获取开发关系。 *平台型服务商:平台型服务商可以通过一键授权,获得为商户开发部署小程序的权限,代商户完成小程序的开发部署及上线。 [图片] Step 1 : 基本信息填写 平台名称: 名称尽可能能够和自己的业务方向相似,且不要包含测试等字样。 业务标签:有如下几类,可以按照行业划分,也可以按照功能划分,标签选择自己业务范畴之内的即可。 [图片] 平台简介: 这里将显示在详情页中,一般用户从这里获得对平台的业务认知,建议要合理填写。 官方网站: 此处填写你的官网域名,需要注意的是,此处写的地址必须要和此第三方平台的业务描述相一致。否则在审核的时候会被驳回的。 平台图标: 需要自己制作核实的图标,必须保证是108*108像素,仅支持PNG格式,大小不超过300KB。 Step 2 :选择权限 微信开放平台为第三方平台型服务商提供丰富的权限勾选,服务商自由选择需要的权限,所勾选的权限,将在用户授权页面展示。最终服务商可获得的权限,以授权用户选择的权限为准。关于权限详情,可查询第三方平台权限说明 特别提醒,请勿随意全选所有权限,当选择一些没有用到的权限集时,会在全网发布的自动化测试时触发某些检测,导致不能通过。具体可以细看全网发布接入检测说明 [图片] Step 3 :开发资料填写 [图片] 授权发起页域名: 此处填写的是跳转授权页时所在的域名,比如此处填写 w.example.com ,那么授权页必须从这个域名中打开才能正常显示,否则会出错。这在一定程度上防止了授权信息的盗用和滥授权,一般这里填用户授权可以访问的业务域名。这里不校验https,但是需要保证域名是实名且可以公共访问的。 授权测试公众号列表: 由于在创建后并没有全网发布,需要先行做开发测试,这里就是用于测试的公众号、小程序的原始ID。我们需要找到一个任意主体的小程序用来测试,推荐选择自己的,好在接下来的环节中实践。需要注意的是,在全网发布之前,扫描授权页二维码授权的账号只能在这个列表中选择,列表外的没有办法成功授权! 授权事件接收URL: 此处需要填写一个API地址,开放平台向你发送的所有消息均通过请求此URL来完成。在这个地址请求后,需要程序判定消息的类型以及相应的处理方法。在此实践中,此URL主要用来接收开放平台的验证票据(component_verify_ticket) 信息校验Token和消息加解密Key: 这两个主要用来解密开放平台向接收URL发送的消息,或者加密信息向开放平台发送。token填写任意一个字符串即可,key必须保证长度为43位的字符串,只能是字母和数字。 小程序服务器域名: 在授权过来的小程序中配置的合法域名必须在此设置的列表中,本教程小程序使用云开发作为后端服务,所以此处设置的域名没有用到,在使用传统后端服务模式时,建议填写支持小程序后台服务的域名。 小程序业务域名: 在授权过来的小程序中配置的合法域名必须在此设置的列表中,本教程小程序使用云开发作为后端服务,所以此处设置的域名没有用到,在使用传统后端服务模式时,建议填写支持小程序后台服务的域名。 白名单IP地址: 这两个主要用来解密开放平台向接收URL发送的消息,或者加密信息向开放平台发送。token填写任意一个字符串即可,key必须保证长第三方平台所有的API接口必须在白名单列表中的IP才可以成功响应,一般填写自己服务器的公网IP。 更多信息可查询文档申请资料说明 完成上述【基本信息】、【选择权限】和【开发资料】信息填写后,提交审核即可,审核时间1小时以内。审核成功后,就可以使用相关接口进行对应开发。 3.3 平台型服务商代实现小程序基本流程 3.3.1开发准备 1)配置域名并设定授权事件url 2)创建授权事件接收URL的服务监听 微信开放平台向授权事件url发送相关的开发或通知信息,如验证票据等。开放平台向此URL发送的数据是加密过的,具体遵循消息加解密方案。 3)使用接收到的验证票据(component_verify_ticket)获取令牌 令牌是第三方平台接口的调用凭据,根据官方文档-获取令牌可知,我们需要请求API接口:https://api.weixin.qq.com/cgi-bin/component/api_component_token 在请求时需要提供三个参数:component_appid(第三方平台 appid)、component_appsecret(第三方平台 appsecret)、component_verify_ticket(第二步获取的ticket),其中前两个我们可以在开放平台直接获取。 [图片] 4)使用令牌获取预授权码并拼接用户授权链接 预授权码(pre_auth_code)是第三方平台方实现授权托管的必备信息,每个预授权码有效期为 10 分钟。一般我们获取它用来拼接授权链接并发给用户使用。 5)获取第三方调用令牌 3.3.2 小程序模版开发 平台型服务商需要将现成小程序模板通过接口直接上传部署到用户的小程序账号中。具体操作为我们在第三方平台的控制台中可以添加开发小程序。 [图片]选择自己为管理者的小程序账号。在添加流程中,也是需要验证小程序账号密码并通过管理员微信扫码验证的。(这里是输入谁的小程序账号密码?) [图片] 当添加成功之后,我们便可以在公众平台的设置页中看到如下信息的变化,开发小程序所属账号为我们的第三方平台的账号,此时小程序的代码上传便由第三方平台接管。 [图片] 使用微信开发者工具使用此小程序appid创建或导入项目,效果如下,跟普通开发相比,多了一个平台名 [图片] 当我们上传代码时,会提示上传到第三方平台的草稿箱。 [图片] 当上传之后,我们便可以在第三方平台详情页中看到我们上传的代码。 [图片]我们可以点击右边的按钮,直接将代码直接添加到模板库中,也可以删除。 [图片]添加后在模板库中就会出现模板代码,并附带有模板编号,我们便可以通过对应的API接口直接将此模板小程序上传至指定的授权小程序中了,无需再用开发者工具上传提交。 具体关于模板小程序的细则,可以查看官方文档。其中关于extAppid的开发调试属于小程序开发的范畴知识附加,在这里不详细阐述。 3.3.3 小程序模板部署与发布(提审) 小程序模版开发完成后,可以点击全网发布按钮。 [图片] 在发布前会进行自动化测试检查,在本教程推荐的两个权限集勾选,无其他权限集的状态下,只需要验证票据的推送状态。具体可以细看全网发布接入检测说明。 检测通过后,会进行资质审核。主要检查你的官网描述是否和设置的描述相仿。审核通过后,第三方平台业务即可上线,可向所有符合要求的公众号、小程序进行登录授权。
2021-09-09 - 第三方平台快速创建小程序接口创建的小程序怎么接入微信支付
[图片] 请教一下,调用微信第三方平台提供的这个接口创建的小程序怎么接入微信支付?怎么获取AppSecret?文档中没有相关的说明。没有AppSecret我怎么取用户的openid?
2019-01-18 - Java中的微信支付(1):API V3版本签名详解
原文出处 1. 前言 最近在折腾微信支付,证书还是比较烦人的,所以有必要分享一些经验,减少你在开发微信支付时的踩坑。目前微信支付的API已经发展到V3版本,采用了流行的Restful风格。 今天来分享微信支付的难点——签名,虽然有很多好用的SDK但是如果你想深入了解微信支付还是需要了解一下的。 2. API证书 为了保证资金敏感数据的安全性,确保我们业务中的资金往来交易万无一失。目前微信支付第三方签发的权威的CA证书(API证书)中提供的私钥来进行签名。通过商户平台你可以设置并获取API证书。 切记在第一次设置的时候会提示下载,后面就不再提供下载了,具体参考说明。 设置后找到[代码]zip[代码]压缩包解压,里面有很多文件,对于JAVA开发来说只需要关注[代码]apiclient_cert.p12[代码]这个证书文件就行了,它包含了[代码]公私钥[代码],我们需要把它放在服务端并利用Java解析[代码].p12[代码]文件获取公钥私钥。 务必保证证书在服务器端的安全,它涉及到资金安全。 解析API证书 接下来就是证书的解析了,证书的解析有网上很多方法,这里我使用比较“正规”的方法来解析,利用JDK安全包的[代码]java.security.KeyStore[代码]来解析。 微信支付API证书使用了[代码]PKCS12[代码]算法,我们通过[代码]KeyStore[代码]来获取公私钥对的载体[代码]KeyPair[代码]以及证书序列号[代码]serialNumber[代码],我封装了工具类: [代码]import org.springframework.core.io.ClassPathResource; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; /** * KeyPairFactory * * @author dax * @since 13:41 **/ public class KeyPairFactory { private KeyStore store; private final Object lock = new Object(); /** * 获取公私钥. * * @param keyPath the key path * @param keyAlias the key alias * @param keyPass password * @return the key pair */ public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(keyPath); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("PKCS12"); store.load(resource.getInputStream(), pem); } } } X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias); certificate.checkValidity(); // 证书的序列号 也有用 String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); // 证书的 公钥 PublicKey publicKey = certificate.getPublicKey(); // 证书的私钥 PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem); return new KeyPair(publicKey, storeKey); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); } } } [代码] 眼熟的可以看出是胖哥Spring Security教程中JWT用的公私钥提取方法的修改版本,你可以对比下不同之处。 这个方法中有三个参数,这里必须要说明一下: [代码]keyPath[代码] API证书[代码]apiclient_cert.p12[代码]的[代码]classpath[代码]路径,一般我们会放在[代码]resources[代码]路径下,当然你可以修改获取证书输入流的方式。 [代码]keyAlias[代码] 证书的别名,这个微信的文档是没有的,胖哥通过加载证书时进行DEBUG获取到该值固定为[代码]Tenpay Certificate[代码] 。 [代码]keyPass[代码] 证书密码,这个默认就是商户号,在其它配置中也需要使用就是[代码]mchid[代码],就是你用超级管理员登录微信商户平台在个人资料中的一串数字。 3. V3签名 微信支付V3版本的签名是我们在调用具体的微信支付的API时在HTTP请求头中携带特定的编码串供微信支付服务器进行验证请求来源,确保请求是真实可信的。 签名格式 签名串的具体格式,一共五行一行也不能少,每一行以换行符[代码]\n[代码]结束。 [代码]HTTP请求方法\n URL\n 请求时间戳\n 请求随机串\n 请求报文主体\n [代码] HTTP请求方法 你调用的微信支付API所要求的请求方法,比如APP支付为[代码]POST[代码]。 URL 比如APP支付文档中为[代码]https://api.mch.weixin.qq.com/v3/pay/transactions/app[代码],除去域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有’?'和对应的查询字符串。这里为[代码]/v3/pay/transactions/app[代码]。 请求时间戳 服务器系统时间戳,保证服务器时间正确并利用[代码]System.currentTimeMillis() / 1000[代码]获取即可。 请求随机串 找个工具类生成类似[代码]593BEC0C930BF1AFEB40B4A08C8FB242[代码]的字符串就行了。 请求报文主体 如果是GET请求直接为空字符[代码]""[代码] ;当请求方法为[代码]POST[代码]或[代码]PUT[代码]时,请使用真实发送的[代码]JSON[代码]报文。图片上传API,请使用[代码]meta[代码]对应的[代码]JSON[代码]报文。 生成签名 然后我们使用商户私钥对按照上面格式的待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。对应的核心Java代码为: [代码]/** * V3 SHA256withRSA 签名. * * @param method 请求方法 GET POST PUT DELETE 等 * @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1 * @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致 * @param nonceStr 随机字符串 要和TOKEN中的保持一致 * @param body 请求体 GET 为 "" POST 为JSON * @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥 * @return the string */ @SneakyThrows String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) { String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body) .collect(Collectors.joining("\n", "", "\n")); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(keyPair.getPrivate()); sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return Base64Utils.encodeToString(sign.sign()); } [代码] 4. 使用签名 签名生成后会同一些参数组成一个[代码]Token[代码]放置到对应HTTP请求的[代码]Authorization[代码]请求头中,格式为: [代码]Authorization: WECHATPAY2-SHA256-RSA2048 {Token} [代码] [代码]Token[代码]由以下五部分组成: 发起请求的商户(包括直连商户、服务商或渠道商)的商户号[代码]mchid[代码] 商户API证书序列号[代码]serial_no[代码],用于声明所使用的证书 请求随机串[代码]nonce_str[代码] 时间戳[代码]timestamp[代码] 签名值[代码]signature[代码] [代码]Token[代码]生成的核心代码: [代码]/** * 生成Token. * * @param mchId 商户号 * @param nonceStr 随机字符串 * @param timestamp 时间戳 * @param serialNo 证书序列号 * @param signature 签名 * @return the string */ String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) { final String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""; // 生成token return String.format(TOKEN_PATTERN, wechatPayProperties.getMchId(), nonceStr, timestamp, serialNo, signature); } [代码] 将生成的[代码]Token[代码]按照上述格式放入请求头中即可完成签名的使用。 5. 总结 本文我们对微信支付V3版本的难点签名以及签名的使用进行了完整的分析,同时对API证书的解析也进行了讲解,相信能够帮助你在支付开发中解决一些具体的问题。后面有时间我还将对签名的验证进行讲解,关注:码农小胖哥 及时获取系列知识。 完整逻辑请参阅微信支付V3 Java开发包项目Payment Spring Boot,欢迎Star
2021-01-19