支付切换后异步回调验签失败如何排查
结论
仅凭题目给出的回调参数无法完成验签,因为其中缺少 sign 和 sign_type。异步通知验签需要支付宝实际 POST 的全部参数,以及与当前接入方式对应的支付宝公钥或支付宝公钥证书。
切换支付接入方式后出现验签失败,常见原因有:
- 采集的回调参数不完整,遗漏了
sign或sign_type。 - 仍在使用旧接入方式对应的支付宝公钥。
- 将应用公钥或应用私钥当成了支付宝公钥。
- 混用了普通公钥模式和证书模式。
- 验签前重新序列化、转义或修改了参数值。
- 使用的字符集或签名算法与实际通知不一致。
当前示例没有签名值,因此无法确定是哪个字段导致验签失败,也无法在本地复现验签结果。
正确的排查方法
1. 保存原始回调请求
先记录支付宝服务器实际发送的以下内容:
- HTTP Method
Content-Type- 原始请求体
- 解析后的全部参数名
signsign_type- 当前使用的公钥或证书序列号
异步通知通常通过表单提交。只记录业务参数无法复现验签问题。
日志中至少应该能看到:
sign=实际签名值
sign_type=RSA2
题目给出的参数以 version=1.0 结束,没有 sign 和 sign_type。如果实际请求中也缺少这两个字段,应检查网关、反向代理、回调参数绑定对象或日志脱敏逻辑是否将其丢弃。
2. 直接使用回调参数 Map 验签
不要根据数据库中的订单字段重新拼装验签字符串,也不要只选择看起来有用的参数。
应读取回调请求的全部参数,按原值放入 Map<String, String>,再交给支付宝 SDK。SDK 会按照签名规则处理 sign、sign_type、参数排序和拼接。
3. 确认使用的是支付宝公钥
验签需要使用支付宝公钥,不能使用以下密钥或证书:
- 应用私钥
- 应用公钥
- 商户自己的证书公钥
- 支付宝开放平台中其他应用的公钥
如果更换了应用、签约主体或接入方式,需要重新确认通知所属的 app_id。本次回调中的值是:
app_id=2021005194677787
验签使用的支付宝公钥或支付宝公钥证书,必须与该应用当前配置的接入模式一致。
4. 区分普通公钥模式和证书模式
如果当前应用使用普通公钥模式,应传入支付宝开放平台提供的支付宝公钥:
AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
charset,
signType
);
如果当前应用使用证书模式,应通过支付宝公钥证书验签:
AlipaySignature.rsaCertCheckV1(
params,
alipayPublicCertPath,
charset,
signType
);
证书模式下不能继续使用旧的普通支付宝公钥,也不能将 appCertPublicKey_*.crt 当成支付宝公钥证书。验签需要的是类似下面的文件:
alipayCertPublicKey_RSA2.crt
具体文件名以实际下载的证书包为准。
Java 验签示例
普通公钥模式
import com.alipay.api.internal.util.AlipaySignature;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public boolean verifyNotify(
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 = values == null ? "" : String.join(",", values);
params.put(entry.getKey(), value);
}
String charset = params.getOrDefault("charset", "UTF-8");
String signType = params.getOrDefault("sign_type", "RSA2");
if (!params.containsKey("sign") || params.get("sign").isEmpty()) {
throw new IllegalArgumentException("异步通知缺少 sign 参数");
}
return AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
charset,
signType
);
}
证书模式
import com.alipay.api.internal.util.AlipaySignature;
boolean verified = AlipaySignature.rsaCertCheckV1(
params,
"/path/to/alipayCertPublicKey_RSA2.crt",
params.getOrDefault("charset", "UTF-8"),
params.getOrDefault("sign_type", "RSA2")
);
不同版本 SDK 的方法签名可能不同,应以项目实际依赖版本提供的 API 为准,不要仅为套用示例而更换 SDK 版本。
不使用 SDK 时如何构造待验签内容
建议优先使用官方 SDK。如果确实需要手工验签,通常要按以下规则构造待验签字符串:
- 排除
sign和sign_type。 - 排除值为空的参数。
- 按参数名进行字典序排序。
- 按
key=value的形式拼接。 - 使用
&连接各参数。 RSA2使用SHA256withRSA。- 使用支付宝公钥验证
sign。
示意代码如下:
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Map;
import java.util.stream.Collectors;
public boolean verifyRsa2(
Map<String, String> params,
PublicKey alipayPublicKey
) throws Exception {
String signValue = params.get("sign");
if (signValue == null || signValue.isEmpty()) {
throw new IllegalArgumentException("缺少 sign 参数");
}
String content = params.entrySet().stream()
.filter(entry -> !"sign".equals(entry.getKey()))
.filter(entry -> !"sign_type".equals(entry.getKey()))
.filter(entry -> entry.getValue() != null)
.filter(entry -> !entry.getValue().isEmpty())
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(alipayPublicKey);
verifier.update(content.getBytes(StandardCharsets.UTF_8));
return verifier.verify(Base64.getDecoder().decode(signValue));
}
手工实现很容易在字符编码、空值过滤、参数排序和证书解析等环节出错,适合用于定位问题,不建议代替成熟的 SDK。
重点检查参数是否被改写
题目中的以下参数在框架绑定、JSON 解析或日志输出过程中尤其容易发生变化:
fund_bill_list=[{"amount":"0.01","fundChannel":"ALIPAYACCOUNT"}]
subject=技能等级考证培训20250925
gmt_create=2025-09-25 11:33:09
gmt_payment=2025-09-25 11:33:14
验签前不要执行以下操作:
- 将
fund_bill_list解析为对象后重新生成 JSON。 - 改变 JSON 字段顺序、空格或转义形式。
- 对
subject再次进行 URL 编码或解码。 - 删除时间值中的空格。
- 将
0.01转为0.0100。 - 将
0.00转为0。 - 对参数执行
trim()。 - 根据业务对象重新生成参数 Map。
例如,下面两个值的业务含义可能相同,但签名计算会将它们视为不同的字符串:
[{"amount":"0.01","fundChannel":"ALIPAYACCOUNT"}]
[{"fundChannel":"ALIPAYACCOUNT","amount":"0.01"}]
验签必须使用请求解析后得到的原始参数值。
编码问题
回调中声明的字符集是:
charset=UTF-8
因此验签也应使用 UTF-8,同时检查:
- Web 容器是否使用 UTF-8 读取请求参数。
- 请求体是否曾按其他字符集解码。
- 网关是否修改了中文参数。
- 是否对已经解码的值再次执行 URL Decode。
如果验签日志中的 subject 出现乱码,问题发生在验签之前,应先修复请求解码流程。
建议增加的诊断日志
生产环境不能打印私钥,但可以在受控日志中记录:
notify_app_id
sign_type
charset
sign_present
verify_mode
alipay_cert_sn
sorted_parameter_names
verify_result
为了比较参数差异,还可以在测试环境记录最终待验签字符串的 SHA-256 摘要:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
String digest = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256")
.digest(content.getBytes(StandardCharsets.UTF_8))
);
通过摘要可以比较不同节点构造的验签内容,同时避免直接暴露完整的业务数据。
验签通过后仍需进行业务校验
签名通过只能说明通知没有被篡改,不表示系统可以直接更新订单。还需要检查:
app_id是否为预期应用。seller_id是否为当前收款账号。out_trade_no是否存在并属于当前商户。total_amount是否与本地订单金额一致。trade_status是否处于允许处理的状态。- 相同的
trade_no或notify_id是否已经处理。
支付宝可能重复发送异步通知,因此订单更新必须具备幂等性。处理成功后,应按接口要求返回成功响应。验签失败时不能更新支付状态,也不能为了停止重试而返回成功。
针对当前参数,首先应获取完整的原始回调,确认其中是否包含 sign 和 sign_type。然后核对应用 2021005194677787 当前使用的是普通公钥模式还是证书模式,并使用对应的支付宝公钥材料直接调用 SDK 验签。
备注:内容仅供参考。
