记一次企业微信回调的 InvalidKeyException 的问题排查与解决方案

问题背景

在对接企业微信消息回调时,触发 java.security.InvalidKeyException: Illegal key size 异常。

该问题通常出现在使用 AES-256 等高强度加密算法时,Java 默认的加密策略对密钥长度进行了限制。

注意 :若项目中 JDK 版本在 Java 8u161+,可能默认无此限制(需验证)。


原因分析

Oracle JDK 默认的加密策略文件(local_policy.jarUS_export_policy.jar)对密钥长度做了限制:

  • AES 最大支持 128 位密钥(例如 Java 8u151 之前版本)。
  • 当尝试使用 AES-256(256 位密钥)时,触发 InvalidKeyException

解决方案对比

1. 升级 JDK 版本(推荐 ✅)

适用场景:新项目或允许升级环境

  • Java 8u161+:默认启用无限制加密策略。
  • Java 9+:完全移除策略限制,无需配置。

验证方法

java 复制代码
int maxKeyLength = Cipher.getMaxAllowedKeyLength("AES");
System.out.println(maxKeyLength); // 输出 2147483647 表示无限制

2. 手动配置无限制策略

适用场景:无法升级 JDK 的旧版本

方法一:替换 JCE 策略文件

  1. 下载对应版本的 JCE 无限制策略文件

  2. 替换以下文件:

    bash 复制代码
    # JDK 路径示例
    ${JAVA_HOME}/jre/lib/security/local_policy.jar
    ${JAVA_HOME}/jre/lib/security/US_export_policy.jar

方法二:修改 java.security 配置(Java 8u151+ 专属)

  1. 打开文件:

    bash 复制代码
    ${JAVA_HOME}/jre/lib/security/java.security
  2. 取消注释并启用无限制策略:

properties 复制代码
crypto.policy=unlimited  # 删除行首的 # 号

3. 使用 Bouncy Castle 加密库(需自行测试)

适用场景:无权修改 JDK 或需灵活控制加密逻辑

步骤

  1. 添加依赖(Maven):

    xml 复制代码
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk18on</artifactId>
        <version>1.77</version>
    </dependency>
  2. 注册 Bouncy Castle 提供程序

    java 复制代码
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import java.security.Security;
    
    public class Main {
        public static void main(String[] args) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
  3. 使用 Bouncy Castle 实现加解密

    java 复制代码
    // 指定 BC 提供程序
    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", "BC");

⚠️ 避坑提示

  • 若仍报 InvalidKeyException,需检查是否使用了旧版本依赖(如 bcprov-jdk15on)。
  • 确保密钥生成逻辑正确(如企业微信的 EncodingAESKey 需经 Base64 解码)。

PS:本人测试时上述方法依然报错,可能还需要手动导入bcprov-jdk18on证书或者替换其他版本。


最佳实践:企业微信回调加解密的 Bouncy Castle 实现

针对企业微信的 WXBizMsgCrypt 类,使用 CBCBlockCipher 替代原加解密逻辑:

WXBizMsgCrypt 类来自企业微信开发者中心 加解密库

核心代码示例

java 复制代码
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Base64;

public class WXBizMsgCrypt {


/**  
 * 对明文进行加密.  
 * * @param text 需要加密的明文  
 * @return 加密后base64编码的字符串  
 * @throws AesException aes加密失败  
 */  
private String encrypt(String randomStr, String text) throws AesException {  
    ByteGroup byteCollector = new ByteGroup();  
    byte[] randomStrBytes = randomStr.getBytes(CHARSET);  
    byte[] textBytes = text.getBytes(CHARSET);  
    byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);  
    byte[] receiveidBytes = receiveid.getBytes(CHARSET);  
  
    // randomStr + networkBytesOrder + text + receiveid  
    byteCollector.addBytes(randomStrBytes);  
    byteCollector.addBytes(networkBytesOrder);  
    byteCollector.addBytes(textBytes);  
    byteCollector.addBytes(receiveidBytes);  
  
    // ... + pad: 使用自定义的填充方式对明文进行补位填充  
    byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());  
    byteCollector.addBytes(padBytes);  
  
    // 获得最终的字节流, 未加密  
    byte[] unencrypted = byteCollector.toBytes();  
  
    try {  
        KeyParameter keyParam = new KeyParameter(aesKey);  
        CipherParameters params = new ParametersWithIV(keyParam, Arrays.copyOfRange(aesKey, 0, 16));  
  
        // 设置加密模式为AES的CBC模式  
        CBCBlockCipher cipher = new CBCBlockCipher(new AESEngine());  
        cipher.reset();  
        cipher.init(true, params);  
  
        byte[] original = new byte[unencrypted.length];  
        for (int i = 0; i < unencrypted.length; i += 16) {  
            cipher.processBlock(unencrypted, i, original, i);  
        }  
        // 使用BASE64对加密后的字符串进行编码  
        return base64.encodeToString(original);  
    } catch (Exception e) {  
        log.error("加密失败", e);  
        throw new AesException(AesException.EncryptAESError);  
    }  
}  
  
/**  
 * 对密文进行解密.  
 * * @param text 需要解密的密文  
 * @return 解密得到的明文  
 * @throws AesException aes解密失败  
 */  
private String decrypt(String text) throws AesException {  
    byte[] original;  
    try {  
        KeyParameter keyParam = new KeyParameter(aesKey);  
        CipherParameters params = new ParametersWithIV(keyParam, Arrays.copyOfRange(aesKey, 0, 16));  
  
        // 设置解密模式为AES的CBC模式  
        CBCBlockCipher cipher = new CBCBlockCipher(new AESEngine());  
        cipher.reset();  
        cipher.init(false, params);  
  
        byte[] encrypted = Base64.decodeBase64(text);  
        // 分块解密  
        original = new byte[encrypted.length];  
        for (int i = 0; i < encrypted.length; i += 16) {  
            cipher.processBlock(encrypted, i, original, i);  
        }  
    } catch (Exception e) {  
        log.error("解密失败", e);  
        throw new AesException(AesException.DecryptAESError);  
    }  
    String xmlContent;  
    String fromReceiveid;  
    try {  
        // 去除补位字符  
        byte[] bytes = PKCS7Encoder.decode(original);  
        bytes = PKCS7Encoder.decode(bytes);  
        // 分离16位随机字符串,网络字节序和receiveid  
        byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);  
  
        int xmlLength = recoverNetworkBytesOrder(networkOrder);  
  
        xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);  
        fromReceiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),  
                CHARSET);  
    } catch (Exception e) {  
        log.error("解密失败", e);  
        throw new AesException(AesException.IllegalBuffer);  
    }  
  
    // receiveid不相同的情况  
    if (!fromReceiveid.equals(receiveid)) {  
        log.error("receiveid不相同, fromReceiveid{}, receiveid{}", fromReceiveid, receiveid);  
        throw new AesException(AesException.ValidateCorpidError);  
    }  
    return xmlContent;  
}
}

总结

方案 适用场景 优点 缺点
升级 JDK 新项目/允许升级 一劳永逸,无需额外配置 需协调环境升级
修改 JCE 策略文件 旧版本 JDK 维护 一次性配置 需权限修改系统文件
Bouncy Castle 无权限修改环境或需定制化 灵活,代码控制 需引入第三方库

推荐选择

  • 优先升级 JDK 至 1.8u161+ 或 Java 11+。
  • 若环境受限,使用 Bouncy Castle 实现(注意依赖版本和填充逻辑)。

附录

WXBizMsgCrypt 完整代码

Java 复制代码
import lombok.extern.slf4j.Slf4j;  
import org.apache.commons.codec.binary.Base64;  
import org.bouncycastle.crypto.CipherParameters;  
import org.bouncycastle.crypto.engines.AESEngine;  
import org.bouncycastle.crypto.modes.CBCBlockCipher;  
import org.bouncycastle.crypto.params.KeyParameter;  
import org.bouncycastle.crypto.params.ParametersWithIV;  
  
import java.nio.charset.Charset;  
import java.security.SecureRandom;  
import java.util.Arrays;  
  
/**  
 * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).  
 * <ol>  
 *     <li>第三方回复加密消息给企业微信</li>  
 *     <li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>  
 * </ol>  
 * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案  
 * <ol>  
 *     <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:  
 *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>  
 *     <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>  
 *     <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>  
 *     <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>  
 * </ol>  
 */  
@Slf4j  
public class WXBizMsgCrypt {  
  
    private static Charset CHARSET = Charset.forName("utf-8");  
    private Base64 base64 = new Base64();  
    private byte[] aesKey;  
    private String token;  
    private String receiveid;  
  
//    static {  
//        Security.addProvider(new BouncyCastleProvider());  
//    }  
  
    /**  
     * 构造函数  
     * @param token 企业微信后台,开发者设置的token  
     * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey  
     * @param receiveid, 不同场景含义不同,详见文档  
     *  
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */  
    public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {  
        if (encodingAesKey.length() != 43) {  
            throw new AesException(AesException.IllegalAesKey);  
        }  
  
        this.token = token;  
        this.receiveid = receiveid;  
        aesKey = Base64.decodeBase64(encodingAesKey + "=");  
    }  
  
    // 生成4个字节的网络字节序  
    private byte[] getNetworkBytesOrder(int sourceNumber) {  
        byte[] orderBytes = new byte[4];  
        orderBytes[3] = (byte) (sourceNumber & 0xFF);  
        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);  
        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);  
        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);  
        return orderBytes;  
    }  
  
    // 还原4个字节的网络字节序  
    private int recoverNetworkBytesOrder(byte[] orderBytes) {  
        int sourceNumber = 0;  
        for (int i = 0; i < 4; i++) {  
            sourceNumber <<= 8;  
            sourceNumber |= orderBytes[i] & 0xff;  
        }  
        return sourceNumber;  
    }  
  
    // 随机生成16位字符串  
    private String getRandomStr() {  
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  
        SecureRandom random = new SecureRandom();  
        StringBuffer sb = new StringBuffer();  
        for (int i = 0; i < 16; i++) {  
            int number = random.nextInt(base.length());  
            sb.append(base.charAt(number));  
        }  
        return sb.toString();  
    }  
  
    /**  
     * 对明文进行加密.  
     *     * @param text 需要加密的明文  
     * @return 加密后base64编码的字符串  
     * @throws AesException aes加密失败  
     */  
    private String encrypt(String randomStr, String text) throws AesException {  
        ByteGroup byteCollector = new ByteGroup();  
        byte[] randomStrBytes = randomStr.getBytes(CHARSET);  
        byte[] textBytes = text.getBytes(CHARSET);  
        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);  
        byte[] receiveidBytes = receiveid.getBytes(CHARSET);  
  
        // randomStr + networkBytesOrder + text + receiveid  
        byteCollector.addBytes(randomStrBytes);  
        byteCollector.addBytes(networkBytesOrder);  
        byteCollector.addBytes(textBytes);  
        byteCollector.addBytes(receiveidBytes);  
  
        // ... + pad: 使用自定义的填充方式对明文进行补位填充  
        byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());  
        byteCollector.addBytes(padBytes);  
  
        // 获得最终的字节流, 未加密  
        byte[] unencrypted = byteCollector.toBytes();  
  
        try {  
            KeyParameter keyParam = new KeyParameter(aesKey);  
            CipherParameters params = new ParametersWithIV(keyParam, Arrays.copyOfRange(aesKey, 0, 16));  
  
            // 设置加密模式为AES的CBC模式  
            CBCBlockCipher cipher = new CBCBlockCipher(new AESEngine());  
            cipher.reset();  
            cipher.init(true, params);  
  
            byte[] original = new byte[unencrypted.length];  
            for (int i = 0; i < unencrypted.length; i += 16) {  
                cipher.processBlock(unencrypted, i, original, i);  
            }  
            // 使用BASE64对加密后的字符串进行编码  
            return base64.encodeToString(original);  
        } catch (Exception e) {  
            log.error("加密失败", e);  
            throw new AesException(AesException.EncryptAESError);  
        }  
    }  
  
    /**  
     * 对密文进行解密.  
     *     * @param text 需要解密的密文  
     * @return 解密得到的明文  
     * @throws AesException aes解密失败  
     */  
    private String decrypt(String text) throws AesException {  
        byte[] original;  
        try {  
            KeyParameter keyParam = new KeyParameter(aesKey);  
            CipherParameters params = new ParametersWithIV(keyParam, Arrays.copyOfRange(aesKey, 0, 16));  
  
            // 设置解密模式为AES的CBC模式  
            CBCBlockCipher cipher = new CBCBlockCipher(new AESEngine());  
            cipher.reset();  
            cipher.init(false, params);  
  
            byte[] encrypted = Base64.decodeBase64(text);  
            // 分块解密  
            original = new byte[encrypted.length];  
            for (int i = 0; i < encrypted.length; i += 16) {  
                cipher.processBlock(encrypted, i, original, i);  
            }  
        } catch (Exception e) {  
            log.error("解密失败", e);  
            throw new AesException(AesException.DecryptAESError);  
        }  
        String xmlContent;  
        String fromReceiveid;  
        try {  
            // 去除补位字符  
            byte[] bytes = PKCS7Encoder.decode(original);  
            bytes = PKCS7Encoder.decode(bytes);  
            // 分离16位随机字符串,网络字节序和receiveid  
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);  
  
            int xmlLength = recoverNetworkBytesOrder(networkOrder);  
  
            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);  
            fromReceiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),  
                    CHARSET);  
        } catch (Exception e) {  
            log.error("解密失败", e);  
            throw new AesException(AesException.IllegalBuffer);  
        }  
  
        // receiveid不相同的情况  
        if (!fromReceiveid.equals(receiveid)) {  
            log.error("receiveid不相同, fromReceiveid{}, receiveid{}", fromReceiveid, receiveid);  
            throw new AesException(AesException.ValidateCorpidError);  
        }  
        return xmlContent;  
    }  
  
    /**  
     * 将企业微信回复用户的消息加密打包.  
     * <ol>  
     *     <li>对要发送的消息进行AES-CBC加密</li>  
     *     <li>生成安全签名</li>  
     *     <li>将消息密文和安全签名打包成xml格式</li>  
     * </ol>  
     *  
     * @param replyMsg 企业微信待回复用户的消息,xml格式的字符串  
     * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp  
     * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce  
     *     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串  
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */  
    public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {  
        // 加密  
        String encrypt = encrypt(getRandomStr(), replyMsg);  
        // 生成安全签名  
        if (timeStamp == "") {  
            timeStamp = Long.toString(System.currentTimeMillis());  
        }  
  
        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);  
        log.info("发送给平台的签名是: " + signature);  
        // 生成发送的xml  
        return XMLParse.generate(encrypt, signature, timeStamp, nonce);  
    }  
  
    /**  
     * 检验消息的真实性,并且获取解密后的明文.  
     * <ol>  
     *     <li>利用收到的密文生成安全签名,进行签名验证</li>  
     *     <li>若验证通过,则提取xml中的加密消息</li>  
     *     <li>对消息进行解密</li>  
     * </ol>  
     *  
     * @param msgSignature 签名串,对应URL参数的msg_signature  
     * @param timeStamp 时间戳,对应URL参数的timestamp  
     * @param nonce 随机串,对应URL参数的nonce  
     * @param postData 密文,对应POST请求的数据  
     *  
     * @return 解密后的原文  
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */  
    public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)  
            throws AesException {  
  
        // 密钥,公众账号的app secret  
        // 提取密文  
        Object[] encrypt = XMLParse.extract(postData);  
  
        // 验证安全签名  
        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());  
  
        // 和URL中的签名比较是否相等  
        log.info("第三方收到URL中的签名:" + msgSignature);  
        log.info("第三方校验签名:" + signature);  
        if (!signature.equals(msgSignature)) {  
            throw new AesException(AesException.ValidateSignatureError);  
        }  
  
        // 解密  
        return decrypt(encrypt[1].toString());  
    }  
  
    /**  
     * 验证URL  
     * @param msgSignature 签名串,对应URL参数的msg_signature  
     * @param timeStamp 时间戳,对应URL参数的timestamp  
     * @param nonce 随机串,对应URL参数的nonce  
     * @param echoStr 随机串,对应URL参数的echostr  
     *     * @return 解密之后的echostr  
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */  
    public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)  
            throws AesException {  
        String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);  
  
        if (!signature.equals(msgSignature)) {  
            throw new AesException(AesException.ValidateSignatureError);  
        }  
  
        return decrypt(echoStr);  
    }  
}
相关推荐
jackson凌2 分钟前
【Java学习笔记】键盘录入方法
java·笔记·学习
kfepiza5 分钟前
ServletRequestListener 的用法笔记250417
java·java ee
kfepiza8 分钟前
ServletContextListener 的用法笔记250417
java·java ee
一介输生9 分钟前
Spring Cloud实现权限管理(网关+jwt版)
java·后端
Dcs12 分钟前
使用 OpenRewrite 简化 Java 和 SpringBoot 迁移
java
kfepiza12 分钟前
ServletRequestAttributeListener 的用法笔记250417
java·java ee
卓豪终端管理15 分钟前
如何安全地管理固定功能设备?
java·大数据·开发语言·网络·人工智能·安全
小希与阿树19 分钟前
阿里云RAM账号免密登录Java最佳实践
java·数据库·阿里云
何似在人间57542 分钟前
SpringAI+DeepSeek大模型应用开发——3 SpringAI简介
java·ai·大模型开发·spring ai
长安城没有风1 小时前
从入门到精通【MySQL】 JDBC
java·mysql