支付宝支付异步验签:只有公钥内容没有证书如何直接验签?
结论
如果支付宝应用使用“普通公钥模式”,异步通知验签只需要支付宝公钥,无需将公钥制作成证书。调用支付宝 SDK 的 rsaCheckV1 方法时,传入异步通知参数、支付宝公钥、字符集和签名类型即可。
如果应用配置的是“公钥证书模式”,则需要支付宝公钥证书、支付宝根证书等证书文件。两种模式不能混用。
使用支付宝 SDK 验签
下面以 Java 为例,假设支付宝公钥已经保存在配置中:
import com.alipay.api.internal.util.AlipaySignature;
import java.util.Map;
public boolean verifyAlipayNotify(Map<String, String> params) throws Exception {
String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...";
String charset = params.getOrDefault("charset", "UTF-8");
String signType = params.getOrDefault("sign_type", "RSA2");
return AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
charset,
signType
);
}
alipayPublicKey 必须填写支付宝开放平台提供的“支付宝公钥”,不能使用商户的应用公钥或应用私钥。
多数配置项需要的是 Base64 编码后的公钥主体,例如:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
如果拿到的公钥带有 PEM 标记,可以去掉首尾标记和换行:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----
处理后为:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
是否需要去掉 PEM 标记,要看具体 SDK 接口的要求。支付宝 Java SDK 中常见的字符串公钥接口通常接收不带标记的公钥主体。
正确获取异步通知参数
验签失败不一定是公钥有问题,也可能是通知参数在解析时发生了变化。在 Servlet 环境中,可以这样构造参数:
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public Map<String, String> getNotifyParams(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
params.put(name, String.join(",", values));
});
return params;
}
如果项目使用旧版 Servlet API,请将 jakarta.servlet.http.HttpServletRequest 替换为:
import javax.servlet.http.HttpServletRequest;
获取参数后执行验签:
Map<String, String> params = getNotifyParams(request);
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
// 验签失败,不处理业务
return;
}
// 验签通过后继续校验业务参数
字符集和签名类型必须与支付宝侧的实际配置一致。当前常见配置为:
charset=UTF-8
sign_type=RSA2
未确认配置前,不要擅自固定为其他值。
不使用 SDK 时的验签原理
如果无法引入支付宝 SDK,也可以使用标准 RSA API 验签,但必须严格按照支付宝的规则生成待验签字符串:
- 排除
sign和sign_type参数。 - 排除值为空的参数。
- 按参数名的字典序排序。
- 按
key=value&key=value的格式拼接。 - 使用通知指定的字符集转换为字节。
- 对
sign进行 Base64 解码。 RSA2使用SHA256withRSA验签。
Java 示例:
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.stream.Collectors;
public class AlipayRsaVerifier {
public static boolean verify(
Map<String, String> params,
String alipayPublicKey,
String charset,
String signType) throws Exception {
String sign = params.get("sign");
if (sign == null || sign.isEmpty()) {
return false;
}
String content = params.entrySet().stream()
.filter(entry -> !"sign".equals(entry.getKey()))
.filter(entry -> !"sign_type".equals(entry.getKey()))
.filter(entry -> entry.getValue() != null)
.filter(entry -> !entry.getValue().isEmpty())
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
String normalizedKey = alipayPublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] keyBytes = Base64.getDecoder().decode(normalizedKey);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
String algorithm;
if ("RSA2".equalsIgnoreCase(signType)) {
algorithm = "SHA256withRSA";
} else if ("RSA".equalsIgnoreCase(signType)) {
algorithm = "SHA1withRSA";
} else {
throw new IllegalArgumentException("Unsupported sign_type: " + signType);
}
Signature verifier = Signature.getInstance(algorithm);
verifier.initVerify(publicKey);
verifier.update(content.getBytes(Charset.forName(charset)));
return verifier.verify(Base64.getDecoder().decode(sign));
}
}
手动验签更容易受到参数解码、空值处理和字符集差异的影响。生产环境通常应优先使用与接入模式匹配的支付宝官方 SDK。
验签通过后还要校验什么
签名正确只能证明通知内容由对应私钥签署,不能说明当前订单可以直接更新。还需要检查:
app_id是否与当前商户应用的 App ID 一致。seller_id或收款账号是否属于当前商户。out_trade_no是否存在且属于当前业务系统。total_amount是否与订单应付金额一致。trade_status是否属于业务允许处理的状态。- 重复收到同一个通知时,业务处理是否具备幂等性。
金额应使用 BigDecimal 比较,避免使用 double:
import java.math.BigDecimal;
BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));
BigDecimal expectedAmount = new BigDecimal(order.getTotalAmount());
if (notifiedAmount.compareTo(expectedAmount) != 0) {
throw new IllegalStateException("订单金额不一致");
}
业务处理成功后,异步通知接口应返回以下纯文本:
success
如果处理失败或暂时无法确认,可以返回:
failure
具体的重试行为以当前支付宝接口文档和接入产品的规则为准。
常见错误
公钥用错
异步通知验签使用“支付宝公钥”,不能使用以下内容:
- 应用公钥
- 应用私钥
- 商户证书中的公钥
- 支付宝公钥证书序列号
接入模式混用
普通公钥模式可以直接传入公钥字符串。公钥证书模式需要使用对应的证书初始化方式和证书验签接口,不能随意从证书中复制一段公钥,再按普通公钥模式调用。
参数被二次处理
以下操作可能导致验签失败:
- 对已经解码的参数再次执行 URL Decode。
- 修改空格、加号、换行或 HTML 特殊字符。
- 将参数转换为 JSON 后再验签。
- 使用数据库中保存的参数代替原始通知参数。
- 拼接参数时包含
sign或sign_type。
应直接使用 Web 框架从本次 HTTP 请求中解析出的完整参数集合进行验签,并在验签完成前保持参数值不变。
备注:内容仅供参考。
