AlipaySignature.rsaCheckV1 验签结果为 false
结论
AlipaySignature.rsaCheckV1 返回 false,通常不是因为创建订单和回调使用了不同密钥,而是验签对象有误,或回调参数在处理过程中发生了变化。
异步通知验签需要使用:
- 支付宝返回的全部通知参数
- 支付宝公钥,而不是应用私钥或应用公钥
- 与通知参数一致的字符集,通常是
UTF-8 - 通知中指定的签名算法,通常是
RSA2
创建订单时,商户使用应用私钥签名。支付宝异步通知由支付宝签名,因此商户必须用支付宝公钥验签。这两个环节本来就不使用同一把密钥。
正确的密钥关系
调用支付宝接口时:
商户使用应用私钥签名
支付宝使用应用公钥验签
接收支付宝异步通知时:
支付宝使用支付宝私钥签名
商户使用支付宝公钥验签
因此,下面的调用是错误的:
AlipaySignature.rsaCheckV1(
params,
APP_PRIVATE_KEY,
"UTF-8",
"RSA2"
);
第二个参数应传入支付宝公钥:
AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
"RSA2"
);
如果回调验签使用了和创建订单时完全相同的密钥,问题很可能就在这里。创建订单通常使用应用私钥,回调验签则需要支付宝公钥。
推荐的验签代码
以下示例适用于普通公钥模式。代码中的支付宝公钥必须来自当前应用及当前环境对应的支付宝开放平台配置。
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) 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 = "";
if (values != null && values.length > 0) {
value = String.join(",", values);
}
params.put(entry.getKey(), value);
}
String signType = params.getOrDefault("sign_type", "RSA2");
return AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
signType
);
}
如果项目仍在使用 javax.servlet,只需修改导入:
import javax.servlet.http.HttpServletRequest;
SDK 会按照规则处理 sign、sign_type 等字段并生成待验签内容。不要自行拼接未经验证的参数字符串来代替原始通知参数。
排查步骤
1. 确认使用的是支付宝公钥
先检查以下配置项是否混淆:
APP_PRIVATE_KEY 应用私钥,用于请求签名
APP_PUBLIC_KEY 应用公钥,提供给支付宝验签
ALIPAY_PUBLIC_KEY 支付宝公钥,用于通知验签
异步通知验签使用的是 ALIPAY_PUBLIC_KEY。
同时确认公钥属于当前应用。不同应用之间,以及沙箱环境和生产环境之间,密钥都不能混用。
2. 确认公钥模式与证书模式一致
普通公钥模式可以调用 rsaCheckV1,并传入支付宝公钥字符串。
如果应用使用公钥证书模式,应根据项目实际依赖的 SDK 版本调用证书验签 API,并使用支付宝公钥证书验签。部分 SDK 提供了类似 rsaCertCheckV1 的方法,具体方法签名以当前 SDK 版本为准。不要从证书中随意复制一段文本,再把它当作普通支付宝公钥传给 rsaCheckV1。
3. 不要修改通知参数
验签参数必须保留支付宝通知到达时的原始值。以下处理都可能导致验签失败:
- 把参数转换为业务对象后再转回
Map - 对参数值进行第二次 URL 解码
- 手动替换
+、空格、换行或%xx - 删除看似无用的通知字段
- 修改金额格式,例如把
100.00改成100 - 修改日期格式
- 重新序列化 JSON 字段
- 在空字符串和
null之间转换 - 使用日志中被截断或脱敏的参数验签
验签参数应尽量直接从 request.getParameterMap() 构造。
4. 检查请求体是否被提前消费或重新解析
支付宝异步通知通常以表单参数发送。如果网关、过滤器或框架提前读取并改写了请求体,控制器最终拿到的参数可能不完整。
排查时可以记录:
- 收到的参数名集合
signsign_typecharsetapp_idtrade_noout_trade_no
日志中的个人信息和敏感业务数据需要脱敏,也不要记录应用私钥。
5. 检查字符集和签名类型
常见的验签配置是:
String charset = "UTF-8";
String signType = "RSA2";
不过,最好仍以实际通知中的 charset 和 sign_type 为准。通知使用 RSA2 时,不要传入 RSA;字符集也不能混用 GBK 和 UTF-8。
如果参数值经过了错误的编码转换,即使肉眼看起来相同,实际字节也可能不同。商品名称、备注等包含中文的字段尤其容易出现这类问题。
6. 检查公钥格式
传给 SDK 的公钥通常应是纯 Base64 正文,例如:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
具体 SDK 是否允许保留以下头尾和换行,需要查看其解析方式:
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
为减少格式问题,应按照当前 SDK 文档的要求保存公钥,并检查配置中心是否存在:
- 多余空格
\r、\n字面量- 内容复制不完整
- 全角字符
- 错误的密钥版本
不要对公钥本身进行 URL 编码。
一个容易出错的参数构造方式
下面的写法会丢失同名参数的其他值,也可能掩盖参数解析中的问题:
Map<String, String> params = new HashMap<>();
for (String name : request.getParameterMap().keySet()) {
params.put(name, request.getParameter(name));
}
支付宝通知参数通常是单值,但按照 SDK 的常见示例完整合并参数值会更稳妥:
Map<String, String[]> requestParams = request.getParameterMap();
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
params.put(entry.getKey(), String.join(",", entry.getValue()));
}
不要先对 request.getParameter(...) 的结果调用 URLDecoder.decode()。Servlet 容器一般已经完成了表单参数解码,再解码一次可能改变原始值。
验签成功后仍需进行业务校验
签名验证通过,只能说明通知内容可以由对应的支付宝公钥验证,不能直接据此更新订单状态。至少还需要核对:
boolean valid = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
params.getOrDefault("sign_type", "RSA2")
);
if (!valid) {
return false;
}
if (!EXPECTED_APP_ID.equals(params.get("app_id"))) {
return false;
}
if (!expectedOutTradeNo.equals(params.get("out_trade_no"))) {
return false;
}
if (!expectedAmount.equals(new BigDecimal(params.get("total_amount")))) {
return false;
}
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)
&& !"TRADE_FINISHED".equals(tradeStatus)) {
return false;
}
金额应使用 BigDecimal 比较,不要使用 double:
BigDecimal expectedAmount = new BigDecimal("100.00");
BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));
if (expectedAmount.compareTo(notifiedAmount) != 0) {
return false;
}
订单更新还要保证幂等,避免支付宝重复发送通知时发生重复入账。所有处理完成后,返回以下纯文本:
success
如果处理失败,不要返回虚假的成功结果。
建议优先检查的三项
可以按实际出现概率先检查以下三项:
rsaCheckV1的第二个参数是否误传了应用私钥或应用公钥,而不是支付宝公钥。- 是否混用了沙箱和生产环境,或者使用了其他应用的支付宝公钥。
- 回调参数是否经过二次 URL 解码、JSON 转换、金额格式化或其他修改。
如果这三项都没有问题,再检查证书模式、字符集、公钥格式,以及网关或过滤器对请求参数的处理。
备注:内容仅供参考。
