微信自定义机器人后台express消息解码插件

最近在搭建自己的微信公众号机器人后台,用户向公众号发送小消息后,会被微信的服务端转发到自己设置的服务器,一开始我们可以选择明文模式,但是为了安全考虑,还是会开启安全模式,此模式下,所有的消息会被整体加密,我们需要在服务端解密,而微信官方文档写的不好,也没有nodejs版本的示例代码,于是我结合实例代码做了一版express的插件,记录一下:

微信公众号消息解密插件

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
const crypto = require('crypto');
const { getConfig } = require('../utils/getConfig');
const { includes } = require('lodash');
const parseString = require('xml2js').parseString;

class PKCS7 {
/**
* 删除补位
* @param {String} text 解密后的明文
*/
decode(text) {
let pad = text[text.length - 1]
if (pad < 1 || pad > 32) {
pad = 0
}
return text.slice(0, text.length - pad)
}
/**
* 填充补位
* @param {String} text 需要进行填充补位的明文
*/
encode(text) {
const blockSize = 32
const textLength = text.length
// 计算需要填充的位数
const amountToPad = blockSize - (textLength % blockSize)
const result = Buffer.alloc(amountToPad)
result.fill(amountToPad)
return Buffer.concat([text, result])
}
}

/**
* 微信公众号消息加解密
* 官方文档(写的非常之烂):https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Message_Encryption/Technical_Plan.html
*/
class WXMsgCrypto {
/**
* 以下信息在公众号 - 开发 - 基本配置
* @param {String} token 令牌(Token)
* @param {String} encodingAESKey 消息加解密密钥
* @param {String} appId 公众号的AppId
*/
constructor(token, encodingAESKey, appId) {
if (!token || !encodingAESKey || !appId) {
throw new Error('please check arguments')
}
this.token = token
this.appId = appId

let AESKey = Buffer.from(encodingAESKey + '=', 'base64')
if (AESKey.length !== 32) {
throw new Error('encodingAESKey invalid')
}
this.key = AESKey
this.iv = AESKey.slice(0, 16)
this.pkcs7 = new PKCS7()
}
/**
* 获取签名
* @param {String} timestamp 时间戳
* @param {String} nonce 随机数
* @param {String} encrypt 加密后的文本
*/
getSignature(timestamp, nonce, encrypt) {
const sha = crypto.createHash('sha1')
const arr = [this.token, timestamp, nonce, encrypt].sort()
sha.update(arr.join(''))
return sha.digest('hex')
}
/**
* 对密文进行解密
* @param {String} text 待解密的密文
*/
decrypt(text) {
// 创建解密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv)
decipher.setAutoPadding(false)

let deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()])

deciphered = this.pkcs7.decode(deciphered)
// 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
// 去除16位随机数
const content = deciphered.slice(16)
const length = content.slice(0, 4).readUInt32BE(0)

return {
message: content.slice(4, length + 4).toString(),
appId: content.slice(length + 4).toString()
}
}
/**
* 对明文进行加密
* 算法:Base64_Encode(AES_Encrypt[random(16B) + msg_len(4B) + msg + $appId])
* @param {String} text 待加密明文文本
*/
encrypt(text) {
// 16B 随机字符串
const randomString = crypto.pseudoRandomBytes(16)

const msg = Buffer.from(text)
// 获取4B的内容长度的网络字节序
const msgLength = Buffer.alloc(4)
msgLength.writeUInt32BE(msg.length, 0)

const id = Buffer.from(this.appId)

const bufMsg = Buffer.concat([randomString, msgLength, msg, id])

// 对明文进行补位操作
const encoded = this.pkcs7.encode(bufMsg)

// 创建加密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv)
cipher.setAutoPadding(false)

const cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()])

return cipheredMsg.toString('base64')
}
}

const { token, WechatAppID, EncodingAESKey } = getConfig();

const wxmc = new WXMsgCrypto(token, EncodingAESKey, WechatAppID)


function WXMsgCryptoMiddleware(req, res, next) {
if (includes(req.path, '/wechatAccess') && req.method.toLowerCase() === 'post') {
let query = req.query
let xml = req.body.xml
// 校验
let msgSignature = wxmc.getSignature(query.timestamp, query.nonce, xml.encrypt)
if (msgSignature !== query.msg_signature) {
res.send('服务器出错,请稍后重试!');
} else {
parseString(wxmc.decrypt(xml.encrypt).message, {
explicitArray: false
}, function(err, res) {
req.body.xml = res.xml
next()
})
}
} else {
next();
}
}

module.exports = WXMsgCryptoMiddleware;

使用

1
2
3
4
5
const express = require('express');
const app = express();
const WXMsgCryptoMiddleware = require('./middlewares/WXMsgCrypto');

app.use(WXMsgCryptoMiddleware)

然后在代码中就可以通过req.body.xml获得解密后的消息了