PAYATHON 2026

支付宝支付异步验签:只有公钥内容没有证书如何直接验签?

支付小周

结论

如果支付宝应用使用“普通公钥模式”,异步通知验签只需要支付宝公钥,无需将公钥制作成证书。调用支付宝 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 验签,但必须严格按照支付宝的规则生成待验签字符串:

  1. 排除 sign 和 sign_type 参数。
  2. 排除值为空的参数。
  3. 按参数名的字典序排序。
  4. 按 key=value&key=value 的格式拼接。
  5. 使用通知指定的字符集转换为字节。
  6. 对 sign 进行 Base64 解码。
  7. 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 请求中解析出的完整参数集合进行验签,并在验签完成前保持参数值不变。

备注:内容仅供参考。