PAYATHON 2026

支付宝回调报错 InvalidKeySpecException 如何解决?

支付阿杰

结论

这个异常和回调地址能否访问无关。它表示 Java 在解析 RSA 公钥时,发现公钥内容或编码格式不符合预期。

支付宝异步通知验签必须使用“支付宝公钥”,不能使用“应用公钥”。如果接入的是证书模式,应使用支付宝公钥证书,不能把证书内容当作普通 RSA 公钥传入。

支付成功只能说明应用私钥签名、支付请求等环节基本正常,不能证明回调验签所用的公钥正确。支付请求和回调验签使用的是两个方向的密钥。

最常见的原因

混淆了应用公钥和支付宝公钥

支付宝开放平台接入通常会用到以下密钥:

  • 应用私钥:由商户保存,用于对请求签名。
  • 应用公钥:上传到支付宝开放平台,供支付宝验证应用签名。
  • 支付宝公钥:由支付宝提供,应用用它验证支付宝响应和异步通知。

回调验签需要使用支付宝公钥:

应用请求支付宝:
应用私钥签名 → 支付宝使用应用公钥验签

支付宝回调应用:
支付宝私钥签名 → 应用使用支付宝公钥验签

如果所谓“网页生成的公钥”其实是应用公钥,用它验证回调签名就是配置错误。应进入对应应用的接口加签配置,确认当前使用的加签方式,并获取该应用对应的支付宝公钥。

公钥格式不符合 X509EncodedKeySpec

Java 中常见的 RSA 公钥解析方式是:

new X509EncodedKeySpec(keyBytes)

这里要求输入 X.509 SubjectPublicKeyInfo 格式。以下情况都可能引发:

algid parse error, not a sequence
  • 传入了 PKCS#1 格式的 RSA 公钥。
  • 把私钥误传给了公钥解析代码。
  • 传入了完整证书,而不是证书中的公钥。
  • 公钥复制不完整,或夹带了空格、引号等字符。
  • Base64 内容被配置中心转义或截断。
  • 混用了沙箱环境和正式环境的密钥。
  • 混用了普通公钥模式和证书模式的配置。

把 PKCS#1 公钥交给 X509EncodedKeySpec,是该异常的典型原因。PKCS#1 公钥内部直接保存 modulus 和 exponent,X.509 公钥则包含算法标识结构,所以解析器会在 algorithm identifier 位置报错。

建议的排查顺序

1. 确认当前加签模式

先在支付宝开放平台检查该应用使用的模式:

  • 普通公钥模式
  • 公钥证书模式

这两种模式不能混用。

普通公钥模式需要配置支付宝公钥。证书模式则应按照 SDK 要求,配置应用公钥证书、支付宝公钥证书和支付宝根证书。

2. 确认拿到的是支付宝公钥

不要使用本地生成的应用公钥,也不要从应用私钥重新导出公钥用于回调验签。支付宝公钥应从当前应用的加签配置中获取。

还需要逐项核对:

  • App ID 是否属于当前应用。
  • 是否误用了另一个应用的支付宝公钥。
  • 是否混用了沙箱和正式环境的配置。
  • 部署环境中的实际配置是否与本地一致。

3. 检查公钥内容

标准 PEM 公钥通常类似:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----

如果需要手动交给 Java 解析,应去掉头尾标记和所有空白字符,但不能改动中间的 Base64 数据:

String normalizedKey = publicKey
        .replace("-----BEGIN PUBLIC KEY-----", "")
        .replace("-----END PUBLIC KEY-----", "")
        .replaceAll("\\s+", "");

如果开头是:

-----BEGIN RSA PUBLIC KEY-----

通常表示这是 PKCS#1 公钥,不能直接交给 X509EncodedKeySpec。

如果开头是:

-----BEGIN CERTIFICATE-----

说明拿到的是证书。不能只删除证书头尾,再把中间内容当作普通公钥解析。

4. 单独验证公钥能否被 Java 解析

可以先绕开支付业务,对配置中的公钥进行最小化解析测试:

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class RsaPublicKeyLoader {

    public static PublicKey loadPublicKey(String publicKeyText) throws Exception {
        String normalizedKey = publicKeyText
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s+", "");

        byte[] encoded = Base64.getDecoder().decode(normalizedKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);

        return KeyFactory.getInstance("RSA").generatePublic(keySpec);
    }
}

如果这里仍然抛出 InvalidKeySpecException,问题就在公钥内容或格式上,此时还没有进入支付宝回调验签阶段。

如果公钥可以正常解析,但回调验签仍然失败,再检查以下内容:

  • 使用的是否为支付宝公钥。
  • sign_type 是否一致,例如 RSA2。
  • 字符集是否一致,通常为 UTF-8。
  • 是否完整传入了支付宝回调参数。
  • 验签前是否对参数值做过解码、拼接或修改。

使用支付宝 SDK 验证异步通知

在普通公钥模式下,建议直接调用支付宝 SDK 提供的验签方法,避免自行拼接待验签字符串:

import com.alipay.api.internal.util.AlipaySignature;

import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public boolean verifyNotify(
        HttpServletRequest request,
        String alipayPublicKey) throws Exception {

    Map<String, String> params = new HashMap<>();

    for (Map.Entry<String, String[]> entry
            : request.getParameterMap().entrySet()) {
        String[] values = entry.getValue();
        params.put(
                entry.getKey(),
                values == null ? "" : String.join(",", values)
        );
    }

    return AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            "UTF-8",
            "RSA2"
    );
}

支付宝 Java SDK 的具体包名和方法签名可能因版本不同而变化,应以项目实际依赖的 SDK API 为准。这里最需要注意的是,第二个参数必须传支付宝公钥,不能传应用公钥。

验签通过后,还要继续核对业务参数:

if (!verifyNotify(request, alipayPublicKey)) {
    return "failure";
}

String appId = request.getParameter("app_id");
String outTradeNo = request.getParameter("out_trade_no");
String tradeStatus = request.getParameter("trade_status");
String totalAmount = request.getParameter("total_amount");

// 继续核对 app_id、商户订单号、订单金额、卖家信息和订单状态。
// 所有检查通过并完成幂等处理后,再返回 success。
return "success";

不能因为回调接口收到了请求,就直接更新订单状态。

PKCS#1 公钥的处理

如果已经确认公钥来源正确,只是格式为:

-----BEGIN RSA PUBLIC KEY-----

可以先把它转换为 X.509 SubjectPublicKeyInfo 格式:

openssl rsa \
  -RSAPublicKey_in \
  -in rsa_public_key.pem \
  -pubout \
  -out public_key_spki.pem

转换后的文件通常以这个标记开头:

-----BEGIN PUBLIC KEY-----

但在支付宝回调验签场景中,更稳妥的处理方式仍是从当前应用的支付宝开放平台配置中,重新复制正确的支付宝公钥,而不是转换一个来源尚未确认的公钥。

证书模式的特殊情况

如果应用启用了公钥证书模式,相关配置项通常是证书文件路径或证书内容,包括:

  • 应用公钥证书
  • 支付宝公钥证书
  • 支付宝根证书

不要把 -----BEGIN CERTIFICATE----- 中间的 Base64 内容作为普通 RSA 公钥传给 X509EncodedKeySpec。应按照当前 SDK 的要求,使用证书模式配置和对应的证书验签接口。普通公钥模式与证书模式的初始化代码不同,整套配置必须保持一致。

为什么支付成功仍然会报错

支付流程跑通和回调验签成功是两件不同的事:

  1. 应用使用应用私钥对支付请求签名。
  2. 支付宝使用应用公钥验证请求。
  3. 用户完成支付。
  4. 支付宝向应用发送异步通知。
  5. 应用使用支付宝公钥验证通知签名。

即使第 1~3 步正常,第 5 步仍可能因为支付宝公钥不正确、公钥格式有误或加签模式配置错误而失败。

排查时应先确认,回调验签代码传入的是否为当前环境、当前 App ID 和当前加签模式对应的支付宝公钥。确认无误后,再检查 PEM、Base64、PKCS#1/X.509 格式,以及配置读取过程中是否发生了截断或转义。

备注:内容仅供参考。