支付宝 Java EasySDK 2.1.2 回调验签失败如何解决?
结论
这个异常通常不代表支付失败,而是回调验签时读取到了不合法的 Base64 数据。建议先检查两处:
alipayPublicKey是否配置错误、内容不完整,或误填成了应用公钥。- 回调参数是否经过手工解析或重新编码,导致
sign中的+、/、=被替换、截断或丢失。
pay() 成功只说明应用私钥可以正常生成请求签名,无法证明支付宝公钥配置正确,也无法证明回调参数没有被破坏。
正确配置支付宝公钥
EasySDK 的 alipayPublicKey 必须填写与当前应用签名模式对应的支付宝公钥。不要填写应用公钥或应用私钥。
使用普通公钥模式时,应填写公钥的 Base64 正文,通常不包含以下 PEM 标记:
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
配置示例:
Config config = new Config();
config.protocol = "https";
config.gatewayHost = "openapi.alipay.com";
config.signType = "RSA2";
config.appId = "应用的 App ID";
config.merchantPrivateKey = "应用私钥正文";
config.alipayPublicKey = "支付宝公钥正文";
config.notifyUrl = "https://example.com/alipay/notify";
Factory.setOptions(config);
检查配置时,需要确认:
merchantPrivateKey填写的是应用私钥。alipayPublicKey填写的是支付宝公钥。- 公钥与支付宝开放平台当前生效的应用配置一致。
- 公钥内容完整,没有混入中文空格、引号或其他不可见字符。
- 普通公钥模式与证书模式没有混用。
- 沙箱环境与正式环境的密钥配置没有混用。
如果公钥来自多行文件,可以在加载时删除 PEM 标记和空白字符:
String alipayPublicKey = publicKeyText
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
config.alipayPublicKey = alipayPublicKey;
不要再次对公钥正文执行 Base64 编码。EasySDK 需要的是公钥本身的 Base64 内容,而不是对公钥字符串二次编码后的结果。
正确获取 notify 参数
支付宝通过表单参数提交异步通知。应直接使用 Servlet 解析后的参数,不要自行拆分原始请求体:
@PostMapping("/alipay/notify")
@ResponseBody
public String notify(HttpServletRequest request) {
Map<String, String> parameters = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
if (values != null && values.length > 0) {
parameters.put(name, values[0]);
}
});
boolean verified = Factory.Payment.Common().verifyNotify(parameters);
if (!verified) {
return "failure";
}
// 验签通过后,再校验 app_id、金额、商户订单号和交易状态
return "success";
}
不要手工解析请求体,例如:
String body = request.getReader().lines().collect(Collectors.joining());
String[] items = body.split("&");
手工拆分容易破坏 URL 编码,也可能改变 sign 中的特殊字符。对于已经解析过的 sign,尤其不要再次调用 URLDecoder.decode()。
排查 sign 是否被破坏
验签前可以记录少量必要的诊断信息,例如参数名、sign 是否存在及其字符串长度。生产日志中不宜长期输出完整的回调数据:
String sign = parameters.get("sign");
String signType = parameters.get("sign_type");
if (sign == null || sign.isEmpty()) {
throw new IllegalArgumentException("支付宝回调缺少 sign 参数");
}
if (!"RSA2".equals(signType)) {
throw new IllegalArgumentException("回调 sign_type 与预期不一致:" + signType);
}
System.out.println("notify parameter names: " + parameters.keySet());
System.out.println("sign length: " + sign.length());
同时检查这些情况:
- 网关、反向代理或 Web 框架是否改写了请求体。
+是否被错误地转换成空格。- 参数是否经过二次 URL 解码。
- 传给
verifyNotify()的是否只是部分参数。 - 是否遗漏了
sign或sign_type。 - 验签前是否修改过通知字段的内容。
content=app_id=xxx 只表示 SDK 已开始构造待验签内容,不代表 app_id 有问题。真正触发 DecoderException 的通常是公钥或 sign 在 Base64 解码时出现异常。
检查依赖冲突
确认公钥和回调参数都正确后,再检查项目是否同时引入了多个版本的 Bouncy Castle,例如:
bcprov-jdk15on
bcpkix-jdk15on
EasySDK 和其他安全组件可能间接引入不同版本。版本冲突可能导致加密类在运行时出现异常。可以通过 Maven 依赖树确认最终加载的版本:
mvn dependency:tree -Dincludes=org.bouncycastle
如果依赖树中存在多个版本,应在 pom.xml 中排除冲突的传递依赖,并统一 bcprov 和 bcpkix 的版本。具体版本应以 EasySDK 2.1.2 的实际依赖声明和项目运行环境为准,未确认兼容性前不要随意升级。
建议的排查顺序
- 从支付宝开放平台重新复制支付宝公钥。
- 确认
alipayPublicKey中没有误填应用公钥。 - 删除 PEM 头尾、换行、空格和不可见字符,但不要再次进行 Base64 编码。
- 使用
request.getParameterMap()获取完整的回调参数。 - 确认
sign和sign_type都存在,并且sign没有经过二次解码或修改。 - 最后检查 Bouncy Castle 依赖冲突。
验签通过后,还要核对 app_id、out_trade_no、total_amount、卖家身份和交易状态。验签和业务字段校验全部通过后,才能更新订单状态,并向支付宝返回 success。
备注:内容仅供参考。
