PAYATHON 2026

支付宝 Java EasySDK 2.1.2 回调验签失败如何解决?

支付老李

结论

这个异常通常不代表支付失败,而是回调验签时读取到了不合法的 Base64 数据。建议先检查两处:

  1. alipayPublicKey 是否配置错误、内容不完整,或误填成了应用公钥。
  2. 回调参数是否经过手工解析或重新编码,导致 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 的实际依赖声明和项目运行环境为准,未确认兼容性前不要随意升级。

建议的排查顺序

  1. 从支付宝开放平台重新复制支付宝公钥。
  2. 确认 alipayPublicKey 中没有误填应用公钥。
  3. 删除 PEM 头尾、换行、空格和不可见字符,但不要再次进行 Base64 编码。
  4. 使用 request.getParameterMap() 获取完整的回调参数。
  5. 确认 sign 和 sign_type 都存在,并且 sign 没有经过二次解码或修改。
  6. 最后检查 Bouncy Castle 依赖冲突。

验签通过后,还要核对 app_id、out_trade_no、total_amount、卖家身份和交易状态。验签和业务字段校验全部通过后,才能更新订单状态,并向支付宝返回 success。

备注:内容仅供参考。