4、消息加解密:AES加密原理、消息体加解密模式、安全模式的配置与调试
好,咱们进入第四讲。消息加解密这块,说实在的,是很多开发者第一次接触微信生态时最头疼的部分。我当年第一次对接时,看着官方文档里那一堆 EncodingAESKey、消息体签名、随机数,说实话,头都是大的。
但你别怕。今天咱们把 AES 加密原理、消息体加解密模式、还有安全模式的配置与调试,一层层剥开来讲。我保证,讲完之后你再看官方文档,会觉得很亲切。
4.1 AES加密原理:别被“高级加密标准”吓到
AES,全称 Advanced Encryption Standard,中文叫高级加密标准。说白了,就是一种对称加密算法。什么叫对称加密?就是加密和解密用的是同一把钥匙。
你想想看,微信服务器给你发消息,用一把钥匙加密。你收到后,用同一把钥匙解密。这就叫对称。
微信用的是 AES-256-CBC 模式。这里有两个关键点:
- AES-256:密钥长度是 256 位,也就是 32 个字节。这是目前最安全的级别之一。
- CBC 模式:Cipher Block Chaining,密码分组链接模式。每个明文块在加密前,会先和前一个密文块做异或运算。
核心要点:微信的 EncodingAESKey 经过 Base64 解码后,得到的就是 43 字节的数据。前 32 字节是 AES 密钥,后 11 字节是固定的,可以忽略。
为什么会这样设计?我个人的理解是,微信想用一个字符串(EncodingAESKey)同时承载密钥和校验信息,方便开发者配置。嗯,这里要注意,你拿到的 EncodingAESKey 是 Base64 编码后的,千万别直接当密钥用。
4.2 消息体加解密模式:三种模式怎么选?
微信公众平台提供了三种消息加解密模式。我在项目中遇到过不少团队,一开始图省事选了明文模式,结果上线后被安全团队打回来重做。所以,我建议你从一开始就想清楚。
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 明文模式 | 消息体不加密,直接传输 XML | 仅用于开发调试,绝对不要用于生产环境 |
| 兼容模式 | 消息体同时包含明文和密文 | 过渡阶段,新旧系统并行时使用 |
| 安全模式 | 消息体完全加密,只传输密文 | 生产环境推荐,安全性最高 |
我个人习惯,从一开始就直接用安全模式。为什么?因为兼容模式虽然看起来“友好”,但实际上你要同时处理两套逻辑,反而容易出 bug。我曾经见过一个项目,兼容模式下明文和密文解析不一致,导致用户消息丢失,排查了整整两天。
避坑指南:我曾经在兼容模式下踩过一个坑——微信服务器发来的消息,Encrypt 标签里的内容和明文部分的 Content 不一致。后来发现是签名校验没通过,但兼容模式不会报错,只是默默返回错误。所以,如果你用兼容模式,一定要自己加一层校验。
4.3 安全模式的配置与调试:手把手教你跑通
好,咱们进入实战环节。安全模式的配置,说白了就三步:
- 在微信公众平台后台设置 EncodingAESKey 和 Token
- 在服务器代码中实现解密逻辑
- 验证签名并返回响应
咱们重点讲第二步和第三步。
4.3.1 解密流程拆解
微信服务器发来的密文消息,结构是这样的:
<xml>
<ToUserName><![CDATA[gh_xxx]]></ToUserName>
<Encrypt><![CDATA[密文内容]]></Encrypt>
</xml>
解密时,你需要做这几件事:
- 从 XML 中提取
Encrypt标签的内容 - 用 EncodingAESKey 解码后的密钥进行 AES-256-CBC 解密
- 解密后的数据包含:随机数(16字节)+ 明文消息 + 用户ID
- 验证消息体签名
嗯,这里要注意,解密后的数据格式是固定的:
[16字节随机数][明文消息][用户ID]
用户ID 的长度是固定的,你可以通过解析最后几位来提取。我刚开始写的时候,直接用字符串截取,结果发现用户ID 长度不对。后来才意识到,用户ID 是变长的,需要用 strlen 或者类似的方法先拿到长度。
4.3.2 代码示例(PHP 版)
我习惯用 PHP 做演示,因为微信官方 SDK 最早就是 PHP 写的。但原理是通用的,你换成 Java、Python、Go 都一样。
// 解密函数
function decryptMsg($encryptedMsg, $encodingAesKey, $token, $appId) {
// 1. 解码 EncodingAESKey
$aesKey = base64_decode($encodingAesKey . "=");
// 2. 提取密文
$xml = simplexml_load_string($encryptedMsg, 'SimpleXMLElement', LIBXML_NOCDATA);
$encrypt = (string)$xml->Encrypt;
// 3. Base64 解码密文
$ciphertext = base64_decode($encrypt);
// 4. AES-256-CBC 解密
$iv = substr($aesKey, 0, 16);
$decrypted = openssl_decrypt($ciphertext, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
// 5. 去除 PKCS7 填充
$pad = ord(substr($decrypted, -1));
$decrypted = substr($decrypted, 0, -$pad);
// 6. 提取随机数、明文和用户ID
$random = substr($decrypted, 0, 16);
$content = substr($decrypted, 16);
$xmlLen = unpack("N", substr($content, 0, 4))[1];
$xmlContent = substr($content, 4, $xmlLen);
$fromAppId = substr($content, 4 + $xmlLen);
// 7. 验证 AppId
if ($fromAppId !== $appId) {
throw new Exception("AppId 不匹配");
}
return $xmlContent;
}
重要提醒:上面的代码中,第 5 步的 PKCS7 填充去除很容易被忽略。如果你解密出来的数据末尾有多余的乱码字符,十有八九是填充没处理好。我曾经因为这个 bug,排查了整整一个下午。
4.3.3 签名验证:别漏了这一步
解密完成后,你还需要验证消息体签名。微信服务器会在请求中带三个参数:signature、timestamp、nonce。你需要用 Token、timestamp、nonce 和密文拼接后做 SHA1 哈希,然后和 signature 对比。
function checkSignature($token, $timestamp, $nonce, $encrypt, $signature) {
$tmpArr = array($token, $timestamp, $nonce, $encrypt);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
return $tmpStr === $signature;
}
你想想看,如果签名验证不通过,说明消息可能被篡改过。这时候直接返回错误,不要继续处理。我见过有些同学为了省事,跳过了签名验证,结果被恶意攻击者伪造消息,造成了不小的损失。
4.4 调试技巧:让加解密不再玄学
调试加解密,最怕的就是“解密出来一堆乱码”。我分享几个实用技巧:
- 打印每一步的中间结果:把 Base64 解码后的密钥、IV、密文都打印出来,和官方文档对比。
- 用官方提供的在线工具验证:微信公众平台后台有“消息加解密验证”工具,把你的 EncodingAESKey 和密文贴进去,看能不能解出来。
- 注意字符编码:微信的消息体是 UTF-8 编码的。如果你在解密后转成了 GBK 或其他编码,一定会出问题。
个人经验:我曾经在调试时,发现解密出来的 XML 里多了一个不可见字符。排查了半天,发现是 PKCS7 填充的最后一个字节值不对。后来我写了一个单元测试,专门测试各种长度的消息,确保填充逻辑万无一失。
好了,这一讲的内容就到这里。消息加解密虽然看起来复杂,但只要你理解了 AES-256-CBC 的原理,再按照流程一步步来,其实并不难。下一讲,咱们聊聊如何搭建一个高可用的智能客服后端,敬请期待。