PAYATHON 2026

支付宝异步通知使用官方SDK验签失败如何解决?

支付老李

结论

支付宝异步通知验签失败,一般不是 fund_bill_list 里的斜杠造成的。更常见的原因是参与验签的参数被修改,或者验签时使用的支付宝公钥、证书模式、字符集或签名算法与通知不匹配。

验签时,应直接使用支付宝发送的原始表单参数构造参数集合。只进行一次标准 URL 解码,不修改字段内容,也不要把参数重新序列化为 JSON。最后,调用与当前接入模式匹配的官方 SDK 方法完成验签。

常见原因

1. 把应用公钥当成支付宝公钥

异步通知由支付宝使用支付宝私钥签名,因此商户验签时应使用:

  • 公钥模式:支付宝公钥
  • 证书模式:支付宝公钥证书

应用公钥或应用公钥证书用于支付宝校验商户发送的请求,不能用来校验支付宝通知。

即使证书文件有效,只要传给验签方法的是应用公钥证书,验签仍会失败。

2. 混用公钥模式和证书模式

公钥模式通常调用:

AlipaySignature.rsaCheckV1(
    params,
    alipayPublicKey,
    charset,
    signType
);

证书模式应调用当前 SDK 提供的证书验签方法。例如,部分 Java SDK 使用:

AlipaySignature.rsaCertCheckV1(
    params,
    alipayPublicCertPath,
    charset,
    signType
);

具体方法名和参数要以项目实际使用的支付宝 SDK 版本为准。不能读取证书文件内容后,直接把它当作普通支付宝公钥传给 rsaCheckV1。

3. 验签前对参数进行了二次编码或修改

以下操作都可能改变待验签内容:

  • 再次对参数执行 URL Decode
  • 将参数转为 JSON 后重新解析
  • 对整个请求体执行 replace("\\/", "/")
  • 调用 trim() 删除字段首尾字符
  • 错误地把 + 转成空格,或把空格转成 +
  • 替换 &、=、中文或换行符
  • 删除值为空字符串的字段
  • 修改参数名的大小写
  • 通过业务对象重新生成参数集合

fund_bill_list 是参数值的一部分。JSON 中的 / 或 \/ 应以实际收到的参数值为准,不要为了处理斜杠而进行全局替换。

4. 请求体读取方式有误

支付宝异步通知通常使用 application/x-www-form-urlencoded 提交,不是普通的 JSON 请求。

如果接口只通过 @RequestBody 接收 JSON,或者请求体已经被日志组件、过滤器或网关读取并转换,最终得到的参数可能与原始通知不同。

Java Web 项目可以直接读取请求参数:

Map<String, String[]> requestParams = request.getParameterMap();

不要先把请求体转换成业务对象,再通过业务对象反向构造验签参数。

5. 字符集处理错误

验签字符集必须与支付宝通知中的 charset 一致,常见值是:

utf-8

不要直接套用旧示例中的字符集转换代码:

new String(value.getBytes("ISO-8859-1"), "UTF-8")

如果 Servlet 容器已经按照 UTF-8 正确解析参数,再转换一次会破坏中文内容。

应在首次读取请求参数前设置编码:

request.setCharacterEncoding("UTF-8");

如果网关或容器已经正确完成解码,直接使用 request.getParameterMap() 返回的值即可。

6. sign_type 与验签算法不一致

通知中的 sign_type 通常是 RSA2。验签时不要写成 RSA,也不要根据支付请求中的配置推测,应直接读取通知参数:

String signType = request.getParameter("sign_type");

如果项目只接受 RSA2,可以在验签前检查:

if (!"RSA2".equals(signType)) {
    throw new IllegalArgumentException("Unsupported sign_type: " + signType);
}

7. 参数集合不完整

同名参数可能对应多个值。官方示例通常使用英文逗号连接这些值,只读取第一个值可能导致待验签字符串不一致。

也不要自行删除看起来与业务无关的字段。SDK 会按照支付宝的规则排除 sign、sign_type 等字段并完成排序,调用方无需自己拼接待验签字符串。

8. 应用配置或运行环境不匹配

还要核对以下对应关系:

  • 沙箱通知是否使用沙箱环境对应的支付宝公钥
  • 正式环境是否使用正式应用对应的支付宝公钥或证书
  • 当前 app_id 是否属于配置中的应用
  • 应用是否更换过密钥或证书
  • 多商户或多应用系统是否根据 app_id 选择了正确配置
  • 配置中心是否仍在使用旧公钥缓存
  • 公钥内容是否被截断或带有多余字符

证书没有过期且文件可以正常读取,只能说明文件本身可用,不能证明它与当前通知所属的应用完全匹配。

Java 验签示例

普通支付宝公钥模式

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,
        String alipayPublicKey
) throws Exception {
    request.setCharacterEncoding("UTF-8");

    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 = "";

        if (values != null && values.length > 0) {
            value = String.join(",", values);
        }

        params.put(entry.getKey(), value);
    }

    String charset = params.getOrDefault("charset", "utf-8");
    String signType = params.get("sign_type");

    if (signType == null || signType.isEmpty()) {
        return false;
    }

    return AlipaySignature.rsaCheckV1(
        params,
        alipayPublicKey,
        charset,
        signType
    );
}

这里的 alipayPublicKey 必须是支付宝公钥,不能使用应用公钥或应用私钥。

证书模式

不同版本 SDK 提供的证书验签接口可能不同。部分 Java SDK 可以这样调用:

boolean verified = AlipaySignature.rsaCertCheckV1(
    params,
    alipayPublicCertPath,
    charset,
    signType
);

alipayPublicCertPath 应指向支付宝公钥证书,不能指向应用公钥证书。使用前需要核对当前 SDK 对该方法的定义。

推荐排查步骤

第一步:记录实际收到的内容

在测试环境中记录:

  • 请求的 Content-Type
  • app_id
  • charset
  • sign_type
  • sign
  • 完整的参数名列表
  • 每个参数的原始值
  • 实际选用的公钥或证书配置标识

日志中的订单号、账号等敏感信息需要脱敏,也不要记录商户私钥。

第二步:绕过业务对象直接验签

在 Controller 入口直接通过 request.getParameterMap() 验签。如果这种方式可以通过,而使用 DTO、JSON 或消息队列中的数据验签失败,说明参数在中间流程中发生了转换。

第三步:比较关键字段

重点检查以下字段是否发生变化:

  • subject
  • body
  • buyer_logon_id
  • fund_bill_list
  • passback_params
  • gmt_payment
  • 包含中文、空格、加号、斜杠或转义字符的字段

尤其要检查 passback_params。这个字段可能经过 URL 编码,如果业务代码提前解码或重复解码,验签就会失败。

第四步:确认密钥方向和运行环境

根据通知中的 app_id 依次确认:

  1. 通知属于哪个支付宝应用。
  2. 该应用使用普通公钥模式还是证书模式。
  3. 当前加载的是支付宝公钥还是支付宝公钥证书。
  4. 配置是否来自同一个应用和同一个环境。
  5. 密钥更新后,运行实例是否已经重新加载配置。

第五步:检查网关和过滤器

如果支付宝请求经过 Nginx、API 网关、WAF 或应用过滤器,需要检查这些组件是否做过以下处理:

  • 修改请求编码
  • 解析并重新生成表单
  • 将表单转换成 JSON
  • 规范化斜杠或转义字符
  • 提前消费请求体
  • 修改 +、空格或百分号编码

可以临时让验签接口绕过请求体转换组件进行对比,但不能绕过必要的安全控制。

验签通过后仍需业务校验

签名通过只说明通知可以使用对应的支付宝公钥验证,不能代替业务校验。还需要确认:

if (!expectedAppId.equals(params.get("app_id"))) {
    return false;
}

if (!expectedSellerId.equals(params.get("seller_id"))) {
    return false;
}

if (!expectedAmount.equals(params.get("total_amount"))) {
    return false;
}

金额建议使用 BigDecimal.compareTo() 比较:

BigDecimal expected = new BigDecimal("99.00");
BigDecimal actual = new BigDecimal(params.get("total_amount"));

if (expected.compareTo(actual) != 0) {
    return false;
}

此外,还要校验订单号、商户身份和交易状态,并保证通知处理具有幂等性。

处理成功后,按照支付宝接口要求返回成功响应,例如:

success

如果业务处理失败,应返回失败结果,让支付宝按照通知机制重试。验签失败时不能更新订单状态,也不能仅凭收到 TRADE_SUCCESS 就认定支付可信。

备注:内容仅供参考。