https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html 这篇文章里提到的AES加密策略为 AES/CBC/PKCS7Padding,实测无法解密。下载文中提供的SDK,其中的策略为 AES/CBC/NoPadding,实测可以成功解密。另外原文缺少对iv参数的描述,希望完善。
附针对当前文档提供的示例明文、密文,可得出正确结果的Java解密代码:
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.crypto.symmetric.AES;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.crypto.spec.IvParameterSpec;
import java.util.Arrays;
import java.util.Map;
/**
* 接收消息推送,解密方式为安全模式
*/
@PostMapping
public static String receive(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam("encrypt_type") String encryptType,
@RequestParam("msg_signature") String msgSignature,
@RequestBody Map requestBody) {
log.info("收到微信小程序消息推送:signature={}, timestamp={}, nonce={}, openid={}, encrypt_type={}, msg_signature={}, requestBody={}",
signature, timestamp, nonce, openid, encryptType, msgSignature, requestBody);
/*
请求报文体示例:
{
"ToUserName": "gh_97417a04a28d",
"Encrypt": "+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04="
}
*/
String encrypt = MapUtil.getStr(requestBody, "Encrypt");
// 验签
// 将三个值按字典值排序拼接
String token = "AAAAA";
String[] strings = {token, timestamp, nonce, encrypt};
Arrays.sort(strings);
String textToSign = ArrayUtil.join(strings, "");
// 验签
boolean verify = DigestUtils.sha1Hex(textToSign).equals(msgSignature);
log.info("验签结果:{}", verify);
if (!verify) {
return null;
}
// 解密消息
String encodingAesKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
byte[] aesKey = Base64.decode(encodingAesKey + "=");
byte[] tmpMsg = Base64.decode(encrypt);
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
AES aes = new AES("CBC", "NoPadding", aesKey, iv.getIV());
// fullStr结构:random(16B) + msg_len(4B) + msg + appid,其中
// random(16B)为 16 字节的随机字符串;
final int randomPartSize = 16;
// msg_len 为 msg 长度,占 4 个字节(网络字节序);
final int msgLenPartSize = 4;
// msg为明文;
// appid为小程序Appid。
byte[] fullStr = trimPadding(aes.decrypt(tmpMsg));
// 删除明文补位字符
String random = new String(Arrays.copyOfRange(fullStr, 0, randomPartSize));
int msgLen = ByteBuffer.wrap(Arrays.copyOfRange(fullStr, randomPartSize, randomPartSize + msgLenPartSize)).getInt();
String msg = new String(Arrays.copyOfRange(fullStr, randomPartSize + msgLenPartSize, randomPartSize + msgLenPartSize + msgLen));;
String appid = new String(Arrays.copyOfRange(fullStr, randomPartSize + msgLenPartSize + msgLen, fullStr.length));;
log.info("解密结果:random={}, msg_len={}, msg={}, appid={}", random, msgLen, msg, appid);
// 如无特殊要求一般返回空或success,其他内容则需要加密返回
return "success";
}
/**
* 删除解密后明文的补位字符
*
* @param decrypted 解密后的明文
* @return 删除补位字符后的明文
*/
private static byte[] trimPadding(byte[] decrypted) {
int pad = decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
public static void main(String[] args) {
String signature = "6c5c811b55cc85e0e1b54100749188c20beb3f5d";
String timestamp = "1714112445";
String nonce = "415670741";
String openid = "o9AgO5Kd5ggOC-bXrbNODIiE3bGY";
String encryptType = "aes";
String msgSignature = "046e02f8204d34f8ba5fa3b1db94908f3df2e9b3";
Map requestBody = MapUtil.newHashMap();
requestBody.put("ToUserName", "gh_97417a04a28d");
requestBody.put("Encrypt", "+qdx1OKCy+5JPCBFWw70tm0fJGb2Jmeia4FCB7kao+/Q5c/ohsOzQHi8khUOb05JCpj0JB4RvQMkUyus8TPxLKJGQqcvZqzDpVzazhZv6JsXUnnR8XGT740XgXZUXQ7vJVnAG+tE8NUd4yFyjPy7GgiaviNrlCTj+l5kdfMuFUPpRSrfMZuMcp3Fn2Pede2IuQrKEYwKSqFIZoNqJ4M8EajAsjLY2km32IIjdf8YL/P50F7mStwntrA2cPDrM1kb6mOcfBgRtWygb3VIYnSeOBrebufAlr7F9mFUPAJGj04=");
System.out.println(receive(signature, timestamp, nonce, openid, encryptType, msgSignature, requestBody));
}
帮了大忙了兄弟,顺便贴一下PHP版的。
protected function decryptMessage($postData, $timestamp, $nonce, $msgSignature) { $config = config('wechat'); // 签名验证 $array = [$postData, $config['payment']['token'], $timestamp, $nonce]; sort($array, SORT_STRING); $str = implode($array); $sign = sha1($str); if ($sign !== $msgSignature) { throw new Exception('加密数据签名验证失败'); } try { // AES 密钥解码 $aesKey = base64_decode($config['payment']['aes_key']); if (strlen($aesKey) !== 32) { throw new Exception('AES 密钥长度不正确'); } $iv = substr($aesKey, 0, 16); $base64Decode = base64_decode($postData); // 解密数据 $data = openssl_decrypt($base64Decode, 'aes-256-cbc', $aesKey, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv); // 去除头部信息 $filterHeader = substr($data, 20); // 替换 app_id $appId = preg_quote($config['mini_program']['app_id'], '/'); $content = preg_replace('/' . $appId . '/', '', $filterHeader); // 返回解密后的数据 return json_decode($content, true); } catch (\Throwable $e) { Log::error('Decrypt failed: ' . $e->getMessage()); throw new Exception('解密数据失败'); } }