移动应用升级RSA2后支付宝异步回调验签失败
结论
RSA2 不是一种新的公钥格式,而是支付宝对签名算法的命名:
RSA通常对应SHA1withRSARSA2对应SHA256withRSA
所以,不存在把”支付宝 RSA 公钥转换成 RSA2 公钥”的操作。两者使用的都是 RSA 密钥,主要区别在于摘要算法。
应用从 RSA 切换到 RSA2 时,需要确保开放平台配置、应用私钥、支付宝公钥和代码中的 sign_type 属于同一套正式环境配置。异步通知必须使用支付宝公钥验签,不能使用开发者自行生成的应用公钥。应用公钥用于提交给支付宝,以确认开发者身份,不能用于验证支付宝发来的通知。
正式环境一直验签失败,通常是因为混用了不同环境或应用的密钥,或者在验签前修改了支付宝发送的原始参数。
RSA2 升级时需要调整什么
升级到 RSA2 时,通常需要检查以下配置:
- 使用符合开放平台要求的 RSA 密钥生成应用公钥和应用私钥。
- 在支付宝开放平台对应的应用中配置应用公钥。
- 保存平台展示或下载的支付宝公钥。
- 商户请求使用应用私钥签名,并设置
sign_type=RSA2。 - 支付宝同步响应和异步通知使用支付宝公钥验签。
- 验签代码也要指定
RSA2,不能继续按RSA验证。
如果升级时重新生成并上传了应用公钥,应重新获取该应用当前展示的支付宝公钥,避免继续使用旧应用、沙箱环境或其他密钥配置中的支付宝公钥。
“应用公钥”和”支付宝公钥”不是同一个东西:
| 密钥 | 用途 |
|---|---|
| 应用私钥 | 商户对请求进行签名 |
| 应用公钥 | 配置到支付宝开放平台,供支付宝验证商户请求 |
| 支付宝公钥 | 商户验证支付宝响应和异步通知 |
| 支付宝私钥 | 由支付宝保管,商户无法也不需要获得 |
如果应用使用公钥证书模式,还需要按证书模式加载支付宝公钥证书,不能直接混用普通公钥模式的字符串。
建议的排查顺序
1. 确认正式环境的应用身份
先检查异步通知中的 app_id,确认它与正式环境配置的应用一致。
常见错误包括:
- 使用了沙箱环境的支付宝公钥。
- 使用了另一个应用的支付宝公钥。
- 代码仍在加载旧应用的密钥文件。
- 测试环境和生产环境共用了配置项。
- 发布后,配置中心、环境变量或容器中的密钥没有更新。
回调地址正确只能说明通知到达了服务器,不能证明验签使用了正确的密钥。
2. 只能使用支付宝公钥验签
异步通知的签名由支付宝生成,因此必须使用支付宝公钥验证。
开发者自行生成的是应用公钥,用它验证支付宝通知一定会失败。即使应用公钥与应用私钥是一对,它也只能验证由该应用私钥生成的签名。
3. 检查 sign_type
异步通知参数中通常包含 sign_type。升级后要确认收到的值确实是 RSA2,验签代码也必须使用相同的值。
不要只根据应用配置推测算法,可以记录回调中收到的 sign_type。记录日志时,不要输出完整的敏感业务数据。
4. 使用原始回调参数验签
验签必须使用支付宝通过 HTTP POST 发送的全部业务参数,同时排除:
signsign_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,并确保验签时加载的是当前正式应用对应的支付宝公钥。生产环境长期稳定验签失败时,密钥或环境混用通常比回调地址问题更值得优先排查。
备注:内容仅供参考。
