收藏
回答

小程序消息推送解密文档过时(附正确代码)

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));
}


最后一次编辑于  2024-12-13
回答关注问题邀请回答
收藏

1 个回答

  • 华章之殇
    华章之殇
    04-26

    帮了大忙了兄弟,顺便贴一下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('解密数据失败');
        }
    }
    
    04-26
    有用 1
    回复
登录 后发表内容