PAYATHON 2026

移动应用升级RSA2后支付宝异步回调验签失败

支付小周

结论

RSA2 不是一种新的公钥格式,而是支付宝对签名算法的命名:

  • RSA 通常对应 SHA1withRSA
  • RSA2 对应 SHA256withRSA

所以,不存在把”支付宝 RSA 公钥转换成 RSA2 公钥”的操作。两者使用的都是 RSA 密钥,主要区别在于摘要算法。

应用从 RSA 切换到 RSA2 时,需要确保开放平台配置、应用私钥、支付宝公钥和代码中的 sign_type 属于同一套正式环境配置。异步通知必须使用支付宝公钥验签,不能使用开发者自行生成的应用公钥。应用公钥用于提交给支付宝,以确认开发者身份,不能用于验证支付宝发来的通知。

正式环境一直验签失败,通常是因为混用了不同环境或应用的密钥,或者在验签前修改了支付宝发送的原始参数。

RSA2 升级时需要调整什么

升级到 RSA2 时,通常需要检查以下配置:

  1. 使用符合开放平台要求的 RSA 密钥生成应用公钥和应用私钥。
  2. 在支付宝开放平台对应的应用中配置应用公钥。
  3. 保存平台展示或下载的支付宝公钥。
  4. 商户请求使用应用私钥签名,并设置 sign_type=RSA2。
  5. 支付宝同步响应和异步通知使用支付宝公钥验签。
  6. 验签代码也要指定 RSA2,不能继续按 RSA 验证。

如果升级时重新生成并上传了应用公钥,应重新获取该应用当前展示的支付宝公钥,避免继续使用旧应用、沙箱环境或其他密钥配置中的支付宝公钥。

“应用公钥”和”支付宝公钥”不是同一个东西:

密钥用途
应用私钥商户对请求进行签名
应用公钥配置到支付宝开放平台,供支付宝验证商户请求
支付宝公钥商户验证支付宝响应和异步通知
支付宝私钥由支付宝保管,商户无法也不需要获得

如果应用使用公钥证书模式,还需要按证书模式加载支付宝公钥证书,不能直接混用普通公钥模式的字符串。

建议的排查顺序

1. 确认正式环境的应用身份

先检查异步通知中的 app_id,确认它与正式环境配置的应用一致。

常见错误包括:

  • 使用了沙箱环境的支付宝公钥。
  • 使用了另一个应用的支付宝公钥。
  • 代码仍在加载旧应用的密钥文件。
  • 测试环境和生产环境共用了配置项。
  • 发布后,配置中心、环境变量或容器中的密钥没有更新。

回调地址正确只能说明通知到达了服务器,不能证明验签使用了正确的密钥。

2. 只能使用支付宝公钥验签

异步通知的签名由支付宝生成,因此必须使用支付宝公钥验证。

开发者自行生成的是应用公钥,用它验证支付宝通知一定会失败。即使应用公钥与应用私钥是一对,它也只能验证由该应用私钥生成的签名。

3. 检查 sign_type

异步通知参数中通常包含 sign_type。升级后要确认收到的值确实是 RSA2,验签代码也必须使用相同的值。

不要只根据应用配置推测算法,可以记录回调中收到的 sign_type。记录日志时,不要输出完整的敏感业务数据。

4. 使用原始回调参数验签

验签必须使用支付宝通过 HTTP POST 发送的全部业务参数,同时排除:

  • sign
  • sign_type

不要先把通知转换为业务对象,再从对象中重新组装待验签数据。以下操作都可能改变原始参数,造成验签失败:

  • 对参数值执行 trim()。
  • 把空字符串改成 null,或者丢弃空参数。
  • 重新序列化 JSON 字符串。
  • 改变日期、金额或布尔值的表示方式。
  • 重复进行 URL 解码。
  • 手动替换空格、+、%2B 等字符。
  • 使用错误的字符集读取请求体。
  • 只选择部分业务参数参与验签。

应优先把请求参数直接放入 Map<String, String>,再交给支付宝 SDK 完成排序、拼接和验签。

Java 验签示例

以下示例适用于支付宝 Java SDK 的普通公钥验签方式。具体类名和方法签名可能随 SDK 版本变化,应以项目实际依赖提供的 API 为准。

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 {
    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 = String.join(",", values);
        params.put(entry.getKey(), value);
    }

    String charset = params.getOrDefault("charset", "UTF-8");
    String signType = params.getOrDefault("sign_type", "RSA2");

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

如果使用较早的 Servlet API,导入路径可能是:

import javax.servlet.http.HttpServletRequest;

支付宝公钥通常应保存为不带 PEM 头尾的 Base64 内容,具体取决于所用 SDK 方法的要求。配置值中不应意外混入多余的空格、引号或转义后的换行符。

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...

如果配置文件保存的是以下形式,需要确认 SDK 是否支持 PEM 格式:

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

没有确认 SDK 的格式要求前,不要自行删除或添加头尾。

+ 号被转换为空格的问题

表单参数使用 application/x-www-form-urlencoded 编码时,+ 可能被解析为空格。正常情况下,Servlet 容器会按表单规则处理参数。但如果业务代码读取原始请求体后又手动解码一次,就可能破坏 sign 或业务参数。

应避免下面这种重复解码:

String value = URLDecoder.decode(
        request.getParameter("some_param"),
        StandardCharsets.UTF_8
);

request.getParameter() 通常返回的已经是解析后的值,再调用 URLDecoder.decode() 可能造成二次解码。更稳妥的做法是直接使用请求参数集合,并让官方 SDK 处理验签数据。

验签通过后还要做业务校验

签名验证成功,不代表订单可以直接入账。还应至少检查:

if (!verifyAlipayNotify(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");
String sellerId = request.getParameter("seller_id");

// 继续校验:
// 1. app_id 是否为当前应用
// 2. seller_id 是否为预期收款账号
// 3. out_trade_no 是否存在
// 4. total_amount 是否与本地订单金额一致
// 5. trade_status 是否属于需要处理的状态
// 6. 是否已经处理过该通知

业务处理必须保证幂等。处理成功后,应按接口要求返回成功响应。如果返回内容不符合要求,支付宝可能继续重试通知。

仍然失败时如何定位

可以在安全日志中记录以下信息:

  • 通知中的 app_id
  • 通知中的 sign_type
  • 通知中的 charset
  • 当前运行环境使用的配置版本
  • 支付宝公钥的指纹,不记录完整公钥
  • SDK 返回的验签结果
  • 参数名列表和参数数量
  • 当前使用普通公钥模式还是证书模式

公钥指纹可以用来确认生产实例是否加载了预期密钥,也能避免完整公钥散落在日志中。

还可以使用同一份回调参数进行对照测试。保留首次收到的参数和值,不做业务对象转换,直接调用 SDK 验签。如果这样可以通过,问题通常出在请求解析、字符集处理或参数重组过程中。如果仍然失败,应优先检查支付宝公钥、app_id、运行环境和签名模式是否匹配。

RSA 升级到 RSA2,不需要把公钥”升级成另一种算法的公钥”。需要调整的是整条签名链路,让它统一使用 SHA256withRSA,并确保验签时加载的是当前正式应用对应的支付宝公钥。生产环境长期稳定验签失败时,密钥或环境混用通常比回调地址问题更值得优先排查。

备注:内容仅供参考。