本文记录的是微信支付 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 内置 limited 和 unlimited 两套策略,并通过 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 账号。
- 存档页面展示版本与最终下载文件应再次核对。
- 旧 JDK 只用于隔离的复现环境,不应重新部署到生产。
排查清单
遇到同类异常时,可以依次确认:
- 记录实际 Java Vendor、版本和启动路径。
- 调用
Cipher.getMaxAllowedKeyLength("AES")检查策略。 - 使用同一份脱敏报文在不同 JDK 中做对照测试。
- 检查
java.security、crypto.policy和遗留 JCE Policy 文件。 - 优先升级到受支持的 JDK,并重新验证回调。
- 全面搜索日志,移除密钥和敏感报文输出。