本文记录的是微信支付 API v2 退款通知的 req_info 解密问题。API v3 的回调格式和 AES-256-GCM 解密流程不同,不能直接套用本文结论。

问题现象

项目使用 weixin-java-pay 3.9.0 解析退款通知:

WxPayRefundNotifyResult notifyResult;
try {
    notifyResult = WxPayRefundNotifyResult.fromXML(
            reqBody,
            routeBO.getChannelPublicKey()
    );
} catch (WxPayException ex) {
    log.error("微信退款通知处理失败", ex);
    throw new PendingException(
            FundChannelCodeEnum.RESPONSE_EXTERNAL_CHANNEL_SIGN_FAILED
    );
}

if (Objects.equals(
        WxpayMsgCdEnum.FAIL.getCode(),
        notifyResult.getReturnCode()
)) {
    throw new PendingException(
            FundChannelCodeEnum.REQUEST_EXTERNAL_CHANNEL_FAILED
    );
}

String payNo = notifyResult.getReqInfo().getOutRefundNo();

上线后解密失败,核心异常如下:

com.github.binarywang.wxpay.exception.WxPayException:
    解密退款通知加密信息时出错
Caused by: java.security.InvalidKeyException:
    Illegal key size or default parameters
    at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026)
    at javax.crypto.Cipher.init(Cipher.java:1249)
    at com.github.binarywang.wxpay.bean.notify
        .WxPayRefundNotifyResult.fromXML(...)

同一份通知密文和商户 API 密钥在本地可以正常解密,线上却稳定失败。

排查思路

先排除密钥和报文问题

最初怀疑密钥错误,但同一密钥可以完成其他微信支付交互,而且把脱敏后的线上报文放到本地能够成功解析。

如果密钥内容错误,通常会在实际解密或填充校验阶段失败;这里异常发生在 Cipher.init 的密钥长度权限检查阶段,方向更接近运行环境限制。

对比 Java 运行环境

线上同时存在两个 Java 8 版本:

  • Java 8u101:解密失败,异常与线上日志一致。
  • Java 8u301:同一报文和密钥解密成功。

微信支付 API v2 的退款通知解密需要 AES-256。旧 JRE 使用受限 JCE 策略时,AES 最大允许密钥长度只有 128 位,因此初始化 256 位密钥会抛出 Illegal key size or default parameters

可以直接在目标运行环境检查:

int maxKeyLength = Cipher.getMaxAllowedKeyLength("AES");
System.out.println("AES max key length = " + maxKeyLength);

如果输出 128,说明当前 JCE 策略受限;无限制策略通常会返回一个远大于 256 的值。

解决方案

最终将服务运行环境从 Java 8u101 升级到 Java 8u301,解密恢复正常。

升级 JDK 是优先方案,因为早期 Java 8 除了加密策略限制,还缺少大量后续安全修复。无法立即升级时,才考虑为对应旧版本安装匹配的 JCE Unlimited Strength Policy。

从 JDK 8u161 开始,JDK 内置 limitedunlimited 两套策略,并通过 java.security 中的 crypto.policy 控制。升级安装可能保留旧策略文件,因此不要只根据版本号推断,应该同时检查:

crypto.policy=unlimited

并用 Cipher.getMaxAllowedKeyLength("AES") 验证实际生效结果。参考 Oracle JCA 加密强度配置

日志安全问题

原始排查代码曾把完整通知报文和商户密钥写入错误日志:

log.error(
        "微信退款通知处理失败 reqBody {} mchKey {}",
        reqBody,
        merchantKey,
        ex
);

这会把支付数据和核心密钥暴露给日志系统,风险远高于解密失败本身。生产代码应改为只记录可用于关联的非敏感字段:

log.error(
        "微信退款通知解密失败, merchantId={}, notifyId={}",
        merchantId,
        notifyId,
        ex
);

商户 API 密钥不应以任何形式写入日志。通知 Body 如确需留存,也应进行字段脱敏、访问控制和生命周期管理。

获取旧 JDK 复现

旧版本 Oracle JDK 可以从 Java Archive 获取:

Oracle Java Archive

选择旧版 JDK 8

需要注意:

  • 下载旧版本通常需要 Oracle 账号。
  • 存档页面展示版本与最终下载文件应再次核对。
  • 旧 JDK 只用于隔离的复现环境,不应重新部署到生产。

排查清单

遇到同类异常时,可以依次确认:

  • 记录实际 Java Vendor、版本和启动路径。
  • 调用 Cipher.getMaxAllowedKeyLength("AES") 检查策略。
  • 使用同一份脱敏报文在不同 JDK 中做对照测试。
  • 检查 java.securitycrypto.policy 和遗留 JCE Policy 文件。
  • 优先升级到受支持的 JDK,并重新验证回调。
  • 全面搜索日志,移除密钥和敏感报文输出。