本篇是对微信官方文档进行补充,一般需要消息解密的场景主要有如下几种:
- 第三方平台的消息推送:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/message_push.html
- 网站应用的消息推送:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/message_push.html
- 移动应用的消息推送:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/message_push.html
- 公众号接收用户消息:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
- 公众号接收事件推送:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
- 小程序接收用户消息和事件:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/receive.html
你需要按照各自指引文档,开放你的服务器 http 服务,通过在管理后台配置你服务器的指定路径和加密参数,来和微信服务确定消息的沟通事宜。
一般涉及到加密消息的只有 Token 和 EncodeingAESKey 两个,只需要保证你的服务器加解密的信息和微信服务的一致即可,所以这个你可以指定。
指定加密信息和服务器路径之后,微信服务器会按照你的加密信息对原始数据进行加密,然后向你指定的 http 服务路径中发送 post 请求。
当你的服务器收到微信服务器发来的请求时,你应该能得到直接拼在请求路径里的 query 参数
比如你的路径为:https://www.example.com/wxmp/
那么实际收到的应该是:
https://www.example.com/wxmp/?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249×tamp=1714036504&nonce=1514711492
你可以将 query 参数拿出来,以备后面解密使用。
另外在请求的 body 中,是一个 base64 编码数据(如果你选择的明文模式,收到的可能是明文)
你需要先解析一下这个 base64 数据,得到一个 xml 或者 json 类型的明文数据,里面有两个东西:
- Appid,这个消息应该所属的应用id(小程序、公众号、第三方平台、移动应用、网站应用、企业微信等)
- Encrypt,真正的消息体(加密过的,我们后面需要解密)
接下来我们就对比 Appid 是否是我们的应用,如果是则继续处理消息体;如果不是就直接丢掉。
处理消息体前,我们需要先验证一下真实性,对 Encrypt 内容进行签名(需要用到配置的 Token,以及 query 里的 timestamp、nonce ),看下是否和 query 参数里提供的 signature (前面如果有加密的话,这里应该是 msg_signature)相同,不同则证明被篡改或者解密信息对应,也没必要继续解了。
如果签名一致,我们就可以继续解密 Encrypt 内容。
解密需要用到一开始配置的 EncodeingAESKey ,AES 使用 CBC 模式,具体的可以直接参考代码:
代码包下载:https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip
上面链接代码包里没有 NodeJS,考虑到相当部分的开发者都用 NodeJS 来开发服务端,所以这里提供最小代码,方便参照设计自己的解密模块。
代码可以直接复制下来运行,需要额外安装 xml2js 模块,最后部分为测试实例(里面的 token 和 key 信息都脱敏过,不需要担心)
const crypto = require('crypto')
const xml2js = require('xml2js')
const xmlparser = new xml2js.Parser()
const ALGORITHM = 'aes-256-cbc' // 使用的加密算法
const MSG_LENGTH_SIZE = 4 // 存放消息体尺寸的空间大小。单位:字节
const RANDOM_BYTES_SIZE = 16 // 随机数据的大小。单位:字节
/**
* 解密数据
* @param {*} encryptdMsg 加密消息体
* @param {*} encodingAESKey AES 加密密钥
* @returns
*/
function decode (encryptdMsg, encodingAESKey) {
const key = Buffer.from(encodingAESKey + '=', 'base64') // 解码密钥
const iv = key.subarray(0, 16) // 初始化向量为密钥的前16字节
const encryptedMsgBuf = Buffer.from(encryptdMsg, 'base64') // 将 base64 编码的数据转成 buffer
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) // 创建解密器实例
decipher.setAutoPadding(false) // 禁用默认的数据填充方式
let decryptdBuf = Buffer.concat([decipher.update(encryptedMsgBuf), decipher.final()]) // 解密后的数据
const padSize = decryptdBuf[decryptdBuf.length - 1]
decryptdBuf = decryptdBuf.subarray(0, decryptdBuf.length - padSize) // 去除填充的数据
const msgSize = decryptdBuf.readUInt32BE(RANDOM_BYTES_SIZE) // 根据指定偏移值,从 buffer 中读取消息体的大小,单位:字节
const msgBufStartPos = RANDOM_BYTES_SIZE + MSG_LENGTH_SIZE // 消息体的起始位置
const msgBufEndPos = msgBufStartPos + msgSize // 消息体的结束位置
const msgBuf = decryptdBuf.subarray(msgBufStartPos, msgBufEndPos) // 从 buffer 中提取消息体
return msgBuf.toString() // 将消息体转成字符串,并返回数据
}
/**
* 生成签名
* @param {*} encrypt 加密消息体
* @param {*} timestamp 时间戳
* @param {*} nonce 随机数
* @param {*} token 令牌
* @returns
*/
function genSign (encrypt, timestamp, nonce, token) {
const rawStr = [token, timestamp, nonce, encrypt].sort().join('') // 原始字符串
const signature = crypto.createHash('sha1').update(rawStr).digest('hex') // 计算签名
return signature
}
// --------------------------- 以下为测试代码 ---------------------------
// 与微信后台的 encodingAESKey 保持一致
const encodingAESKey = "KE66t5p56ig2rz66Ep2H6bTiitgP2ib26GbuT9tumBB"
// 与微信后台的 token 保持一致
const token = "fjKCjCksHwYwHZWARPgtjKKYZyRAstkr"
// 你的服务器会收到微信服务器发送的消息,这是 query 参数
const query = {
signature: 'eab668051feb6330f0f84e9a9b0bc8d9d972c049', // 有些平台会对加密消息体再做一层 base64,这里就是此 base64 的签名,否则就是真的加密消息体的签名
timestamp: '1721800206', // 签名要用
nonce: '1727131331', // 签名要用
encrypt_type: 'aes', // 有些平台会有特殊加解密方式,请参考实际文档,默认 aes_cbc
msg_signature: '31c02ec801f178b72e712427a64a59be01ebec5a' // 有些平台会对加密消息体再做一层 base64,所以这里有提供就是真的加密消息体的签名
}
// 你的服务器会收到微信服务器发送的消息,这是 body 参数,绝大部分会是 xml 消息体,有时会做一层 base64 编码
// 需要先 base64 解码,然后得到 xml 解析,xml 中会包含 Appid 和 Encrypt 两个,你需要验证 Appid 是你自己的,并且只取 Encrypt 中的内容就好,下面是内容。
const body = 'Sk+nxj+6bfJYOu1zmckuQhxVPaT9IRHA3C4wmNAJvYa7oBmxBg6nETY8dv7nWw9O8N2VbJYcANjBfDeGXOySKweRlEUHbDEzsHprI3A1WY6H73v4lg97TPyTudYVNDvY6ve4hEwlkxgx8WOl7Jd9HbpuIZJ4711CQuJQNuQyONkBFBXxQXuyIurthDv9/pxKYwnmV/NZQF6LKyuezAfjyoz35lqK7QgnfCW6kAB5wAiYOaW0CK4/4b9jo9J40jPumvs4d9Wa+t4Xzh1a8R6ogCTlw6EQSKGqEbKblSI8p0Dc2QDzEuRO+1qlcnoHmaacf14aL41Y148n6lObt9YPsTWI4CZNA0C4P7mPVsWd1v63vHTk4shI6VanpTruIjhdAEiWbeg+rFU5CQj5+0V0QJpQOQasFW9T4VW2m8s85isB9f4A91Q3+6bAoWlHBqj6c8tAnQzxtjMAJZDX2SNCov+B3kICZke2FLE+OhUVNxnsaETJ++EFUTwE7kOzgSGRen9Shnsi3CHEHSDLtveUavzfS33RWQzRSTGsZc++0ZNKrI1pgVR4Oew4WQgHGDxlRiVPeNvPFMD36V72ZE/0BSLdW3It/9TK34rLTx6xv7952VJ/gYKbTq0LBvvrt6YaFMJdPsnZlix8TUfNXRZN/MNsNJPOj4B+PgkU5duxbYc0bHvcA7oUlWnm3vSpIHaGBBsrsRaMAXLqluky9zM0l4fcS93Cyjcgt/eS7QkZWVpLKrtBkj2OmN0plBJNv8MiRcM/GX0BcHSBlyXeXbO/4vkkTRgKGbjLAWRBff8iP9j0RziTyVZ/i08QO79k0dXPrY/SON4wtVagHdU2FZ8pQA=='
// 你需要自己计算一下消息的签名,并和微信服务器计算出来的签名进行比较。
const signature = genSign(body, query.timestamp, query.nonce, token)
console.log('本地计算的签名:', signature)
console.log('微信服务器计算的签名:', query.msg_signature)
// 如果相同,说明消息是微信服务器发来的,否则说明消息不是微信服务器发来的
if(signature === query.msg_signature){
const result = decode(body, encodingAESKey)
console.log('解密后的数据', result) // 如果是 XML 格式,可能比较难处理,所以下一步转换为 JSON,如果指定微信服务器发送JSON格式,可以忽略
xmlparser.parseStringPromise(result).then(res=>console.log('最终数据', res.xml))
} else {
console.log('签名不一致')
}
板凳
已阅,三连
赞