证书模式支付回调使用 AlipaySignature.rsaCertCheckV1 验签失败
结论
在证书模式下,支付回调必须使用支付宝公钥证书验签。应用公钥证书、应用私钥和支付宝根证书都不能用于这一步。
如果证书通过 PEM 文本 content 配置,还要确认当前支付宝 Java SDK 中的 AlipaySignature.rsaCertCheckV1 能否直接接收证书内容。部分版本要求传入的是证书文件路径。此时如果把 PEM 内容作为参数传入,SDK 会将其当作路径处理,导致验签失败。
建议按以下顺序排查:
- 确认使用的是支付宝公钥证书。
- 根据回调中的
alipay_cert_sn选择匹配的证书。 - 原样获取全部回调参数,不要二次解码或修改参数值。
- 按回调声明的字符集和签名类型验签,通常是
UTF-8和RSA2。 - 如果
rsaCertCheckV1只接受证书路径,就传入证书文件路径。若业务只能保存证书内容,可以从证书中提取支付宝公钥,再调用rsaCheckV1。
常见原因
1. 用错证书
证书模式通常涉及四类材料:
- 应用私钥:用于应用请求签名。
- 应用公钥证书:用于标识和验证应用公钥。
- 支付宝公钥证书:用于验证支付宝回调签名。
- 支付宝根证书:用于校验证书链,不能代替支付宝公钥证书完成业务回调验签。
异步通知由支付宝签发,因此验签时必须使用支付宝公钥证书。
2. 把证书内容传给只接受路径的方法
典型调用如下:
boolean verified = AlipaySignature.rsaCertCheckV1(
params,
alipayPublicCertPath,
charset,
signType
);
在部分 SDK 版本中,第二个参数要求传入支付宝公钥证书的文件路径,例如:
String alipayPublicCertPath = "/opt/certs/alipayCertPublicKey_RSA2.crt";
如果传入的是下面这样的 PEM 内容:
-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----
SDK 可能会把整段内容当作文件名,因而无法正确加载证书。
方法是否存在其他重载、具体支持哪些参数,应以项目实际使用的 SDK 版本源码或 API 定义为准,不能只看参数名判断。
3. alipay_cert_sn 与证书不匹配
证书模式的回调通常包含:
alipay_cert_sn
该参数用于标识支付宝本次签名使用的公钥证书。证书更新或轮换后,本地保存的旧证书可能无法与这个序列号匹配。
需要检查:
- 回调中是否包含
alipay_cert_sn。 - 本地保存的支付宝公钥证书是否仍然有效。
- SDK 能否根据
alipay_cert_sn找到匹配的证书。 - 系统是否只保存了一张旧证书,而支付宝已经切换到新证书。
不要直接把 X.509 证书中显示的十进制序列号当作 alipay_cert_sn。支付宝证书序列号有特定的计算规则,建议使用 SDK 提供的证书工具方法进行计算和比较。
4. 回调参数被修改
验签必须使用支付宝回调中的原始参数。以下操作都可能破坏签名:
- 对参数值重复执行 URL Decode。
- 错误地将
+转换为空格。 - 重新格式化 JSON、XML 等参数。
- 删除值为空字符串的参数。
- 将参数放入对象后再次序列化。
- 手工拼接待验签字符串。
- 使用已经读取并转换过编码的请求体。
- 混入网关、框架或业务代码自行添加的参数。
应直接根据 HTTP 请求参数构造 Map<String, String>。
使用证书文件路径验签
如果当前 SDK 的 rsaCertCheckV1 接受证书路径,可以这样处理:
import com.alipay.api.internal.util.AlipaySignature;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public boolean verifyAlipayNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
String[] values = entry.getValue();
String value = values == null ? "" : String.join(",", values);
params.put(entry.getKey(), value);
}
String alipayPublicCertPath =
"/opt/certs/alipayCertPublicKey_RSA2.crt";
String charset = params.getOrDefault("charset", "UTF-8");
String signType = params.getOrDefault("sign_type", "RSA2");
return AlipaySignature.rsaCertCheckV1(
params,
alipayPublicCertPath,
charset,
signType
);
}
alipayPublicCertPath 中不能填写应用公钥证书的路径。
如果项目使用 javax.servlet,将示例中的导包改为:
import javax.servlet.http.HttpServletRequest;
证书只能以内容形式保存时
如果 SDK 没有提供接收证书内容的 rsaCertCheckV1 重载,可以解析支付宝公钥证书,从中提取公钥,然后调用 rsaCheckV1:
import com.alipay.api.internal.util.AlipaySignature;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Map;
public boolean verifyWithCertContent(
Map<String, String> params,
String alipayPublicCertContent) throws Exception {
CertificateFactory certificateFactory =
CertificateFactory.getInstance("X.509");
X509Certificate certificate;
try (ByteArrayInputStream input = new ByteArrayInputStream(
alipayPublicCertContent.getBytes(StandardCharsets.US_ASCII))) {
certificate = (X509Certificate)
certificateFactory.generateCertificate(input);
}
String alipayPublicKey = Base64.getEncoder()
.encodeToString(certificate.getPublicKey().getEncoded());
String charset = params.getOrDefault("charset", "UTF-8");
String signType = params.getOrDefault("sign_type", "RSA2");
return AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
charset,
signType
);
}
使用这种方式前,需要确认回调中的 alipay_cert_sn 与该证书匹配。如果系统保存了多张支付宝公钥证书,应先根据 alipay_cert_sn 选出正确的证书,再提取公钥,不能随意固定使用其中一张。
进一步排查
排查时可以记录以下信息,但不要记录应用私钥:
sign_type
charset
alipay_cert_sn
app_id
notify_id
trade_no
验签结果
所选支付宝公钥证书的序列号
还需要重点核对:
- 回调中的
app_id是否属于当前应用。 - 是否混用了沙箱环境和生产环境的证书。
- 回调使用的是表单参数还是 JSON 请求体。
- 实际传给验签方法的参数是否包含
sign和sign_type。 sign_type是否为RSA2,代码中是否误写为固定的RSA。- 证书内容是否完整保留
BEGIN CERTIFICATE和END CERTIFICATE。 - PEM 内容中是否混入引号、转义字符
\n或额外的配置前缀。 - 支付宝公钥证书是否已经更新。
验签通过后,还要继续校验 app_id、订单号、金额、卖家身份和订单状态。签名正确只能证明通知没有被篡改,不能证明其中的业务数据一定与当前订单一致。
备注:内容仅供参考。
