支付宝异步通知使用官方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_idcharsetsign_typesign- 完整的参数名列表
- 每个参数的原始值
- 实际选用的公钥或证书配置标识
日志中的订单号、账号等敏感信息需要脱敏,也不要记录商户私钥。
第二步:绕过业务对象直接验签
在 Controller 入口直接通过 request.getParameterMap() 验签。如果这种方式可以通过,而使用 DTO、JSON 或消息队列中的数据验签失败,说明参数在中间流程中发生了转换。
第三步:比较关键字段
重点检查以下字段是否发生变化:
subjectbodybuyer_logon_idfund_bill_listpassback_paramsgmt_payment- 包含中文、空格、加号、斜杠或转义字符的字段
尤其要检查 passback_params。这个字段可能经过 URL 编码,如果业务代码提前解码或重复解码,验签就会失败。
第四步:确认密钥方向和运行环境
根据通知中的 app_id 依次确认:
- 通知属于哪个支付宝应用。
- 该应用使用普通公钥模式还是证书模式。
- 当前加载的是支付宝公钥还是支付宝公钥证书。
- 配置是否来自同一个应用和同一个环境。
- 密钥更新后,运行实例是否已经重新加载配置。
第五步:检查网关和过滤器
如果支付宝请求经过 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 就认定支付可信。
备注:内容仅供参考。
