评论

小程序、公众号、第三方平台等场景下,微信服务器推送的消息如何解密|NodeJS

本篇是对微信官方文档进行补充,主要简述了消息推送配置和解密的过程细节,并提供了 NodeJS 示例

本篇是对微信官方文档进行补充,一般需要消息解密的场景主要有如下几种:

你需要按照各自指引文档,开放你的服务器 http 服务,通过在管理后台配置你服务器的指定路径和加密参数,来和微信服务确定消息的沟通事宜。

一般涉及到加密消息的只有 Token 和 EncodeingAESKey 两个,只需要保证你的服务器加解密的信息和微信服务的一致即可,所以这个你可以指定。

指定加密信息和服务器路径之后,微信服务器会按照你的加密信息对原始数据进行加密,然后向你指定的 http 服务路径中发送 post 请求。

当你的服务器收到微信服务器发来的请求时,你应该能得到直接拼在请求路径里的 query 参数

比如你的路径为:https://www.example.com/wxmp/

那么实际收到的应该是:

https://www.example.com/wxmp/?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249&timestamp=1714036504&nonce=1514711492


你可以将 query 参数拿出来,以备后面解密使用。

另外在请求的 body 中,是一个 base64 编码数据(如果你选择的明文模式,收到的可能是明文)

你需要先解析一下这个 base64 数据,得到一个 xml 或者 json 类型的明文数据,里面有两个东西:

  1. Appid,这个消息应该所属的应用id(小程序、公众号、第三方平台、移动应用、网站应用、企业微信等)
  2. 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('签名不一致')
}
最后一次编辑于  07-25  
点赞 1
收藏
评论

3 个评论

  • 启年
    启年
    发表于小程序端
    07-26

    板凳

    07-26
    赞同 1
    回复
  • 拾忆
    拾忆
    07-25

    已阅,三连

    07-25
    赞同 1
    回复
  • showms
    showms
    07-26

    07-26
    赞同
    回复
登录 后发表内容