Spring Boot 沙盒环境下支付宝 SDK 订单查询验签失败
结论
这类问题通常与 rsaCheck 和 rsaCheckV1 的兼容性无关。仅凭调用栈,也无法认定 alipay-sdk-java:4.40.308.ALL 存在缺陷。
这两个方法处理的数据不同:
rsaCheckV1(Map<String, String> params, ...)用于支付宝异步通知、同步跳转等表单参数的验签。它会过滤sign和sign_type,再对参数排序、拼接并验签。rsaCheck(String sourceContent, String signature, ...)用于待验签原文已经确定的场景。开放平台接口返回 JSON 后,SDK 会提取响应节点对应的原始 JSON 文本,再调用该方法验签。
SDK 使用 rsaCheck 验证订单查询响应是正常设计,不应改成 rsaCheckV1。如果查询接口持续验签失败,应先检查支付宝公钥、运行环境、客户端构造参数,以及响应原文是否被修改。
程序进入 RSASignature.engineVerify 本身也不代表发生了异常。这是 Java 执行 RSA 验签时必经的底层方法。需要关注的是该方法最终是否返回 false,以及签名字节长度是否与公钥模长一致。
为什么回调能通过,订单查询却失败
回调验签成功,只能说明传给回调验签方法的参数与公钥能够匹配,不能证明查询客户端使用了同一套配置。
两种验签的数据来源并不相同。
rsaCheckV1:参数集合验签
异步通知通常是一组类似下面的参数:
app_id=...
out_trade_no=...
trade_no=...
sign=...
sign_type=RSA2
rsaCheckV1 会按以下步骤处理:
- 排除
sign和sign_type。 - 按参数名排序。
- 拼接待验签字符串。
- 使用支付宝公钥验证
sign。
因此,它适合处理 Map<String, String> 形式的通知参数。
rsaCheck:确定原文验签
订单查询返回的是 JSON,典型结构如下:
{
"alipay_trade_query_response": {
"code": "10000",
"msg": "Success",
"out_trade_no": "..."
},
"sign": "..."
}
此处的签名对应响应节点的原始内容,而不是通知参数 Map。SDK 解析器会提取:
alipay_trade_query_response 对应的原始 JSON
然后调用:
AlipaySignature.rsaCheck(
sourceContent,
signature,
alipayPublicKey,
charset,
signType
);
如果自行对 JSON 进行反序列化和重新序列化,字段顺序、转义方式、数字格式或空白都可能发生变化。修改后的字符串不能再作为原始响应进行验签。
因此,SDK 内部使用 rsaCheck 是正确的,不能用回调验签所用的 rsaCheckV1 替换。
最应先检查的配置:支付宝公钥
查询响应由支付宝使用支付宝私钥签名,验签时必须使用支付宝公钥。
以下几种密钥容易混淆:
- 应用私钥:供商户对请求签名。
- 应用公钥:上传至支付宝开放平台,供支付宝验证商户请求。
- 支付宝公钥:供商户验证支付宝响应和通知。
- 应用公钥证书、支付宝公钥证书、支付宝根证书:用于证书模式。
DefaultAlipayClient 的 alipayPublicKey 参数必须传入当前应用、当前环境对应的支付宝公钥,不能填写应用公钥。
AlipayClient client = new DefaultAlipayClient(
gatewayUrl,
appId,
appPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
应逐项核对参数位置,尤其要防止配置类字段名称相近,导致 appPublicKey 被传到 alipayPublicKey 的位置。
可以在不输出密钥原文的前提下记录配置指纹:
private static String fingerprint(String value) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(value.trim().getBytes(StandardCharsets.UTF_8));
StringBuilder result = new StringBuilder();
for (byte item : digest) {
result.append(String.format("%02x", item));
}
return result.toString();
}
应用启动时可以记录环境、应用 ID 和公钥指纹:
log.info(
"Alipay config: sandbox={}, gateway={}, appId={}, signType={}, charset={}, alipayKeyFingerprint={}",
useSandbox,
gatewayUrl,
appId,
signType,
charset,
fingerprint(alipayPublicKey)
);
日志中不要打印应用私钥或完整的支付宝公钥。
如何判断 Signature length not correct
如果实际异常为:
Signature length not correct: got ... but was expecting ...
它通常说明解码后的签名字节长度与当前公钥的 RSA 模长不一致。
例如,RSA 2048 位密钥对应的签名字节长度通常是 256 字节。如果验签时使用了不同长度或完全不匹配的公钥,就可能出现长度错误。
建议重点检查:
- 是否误将应用公钥用作支付宝公钥。
- 是否混用了沙盒和生产环境的支付宝公钥。
- 是否使用了另一个
appId对应的支付宝公钥。 - 是否把证书内容错误地作为普通公钥传入。
sign是否被截断、添加了引号或重复进行 Base64 解码。- 配置中心或环境变量是否覆盖了本地配置。
- 系统是否仍在复用旧的单例
AlipayClient。 - 是否有多个同名配置 Bean,导致实际注入的实例与预期不符。
如果签名长度正确,但 engineVerify 返回 false,更可能是以下原因:
- 当前公钥不是签署该响应所用私钥对应的支付宝公钥;
RSA与RSA2配置不一致;- 待验签的响应原文被修改;
- 响应来自其他环境、应用或中间系统。
普通公钥模式和证书模式不能混用
如果开放平台应用采用普通公钥模式,应使用普通公钥客户端,并传入支付宝公钥:
new DefaultAlipayClient(
gatewayUrl,
appId,
appPrivateKey,
"json",
charset,
alipayPublicKey,
signType
);
如果应用已经切换到证书模式,则需要按照当前 SDK 支持的证书客户端配置方式提供:
- 应用私钥;
- 应用公钥证书;
- 支付宝公钥证书;
- 支付宝根证书。
不能从证书中任意复制一段内容,再将其作为普通 alipayPublicKey 使用。普通公钥模式和证书模式必须分别使用与其匹配的配置入口、密钥材料和响应验签流程。
建议的客户端配置方式
沙盒和生产配置应完整隔离,不要只在处理回调时判断环境:
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {
private boolean sandbox;
private Environment sandboxConfig;
private Environment prodConfig;
public Environment current() {
return sandbox ? sandboxConfig : prodConfig;
}
public static class Environment {
private String gatewayUrl;
private String appId;
private String appPrivateKey;
private String alipayPublicKey;
private String charset = "UTF-8";
private String signType = "RSA2";
// getter/setter
}
// getter/setter
}
创建客户端时,只从同一个环境配置对象中读取参数:
@Bean
public AlipayClient alipayClient(AlipayProperties properties) {
AlipayProperties.Environment config = properties.current();
return new DefaultAlipayClient(
config.getGatewayUrl(),
config.getAppId(),
config.getAppPrivateKey(),
"json",
config.getCharset(),
config.getAlipayPublicKey(),
config.getSignType()
);
}
这样可以避免网关来自沙盒配置,而支付宝公钥误用了生产配置。
订单查询代码可以进一步收紧
trade_no 和 out_trade_no 都能标识订单。为减少排查变量,建议优先只传入一个确定有效的标识:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
if (StringUtils.isNotBlank(orderNo)) {
model.setTradeNo(orderNo);
} else {
model.setOutTradeNo(orderId);
}
model.setQueryOptions(
Collections.singletonList("trade_settle_info")
);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
return AjaxResult.success(response);
}
log.warn(
"Alipay query failed: code={}, subCode={}, msg={}, subMsg={}",
response.getCode(),
response.getSubCode(),
response.getMsg(),
response.getSubMsg()
);
return AjaxResult.error(
StringUtils.defaultIfBlank(response.getSubMsg(), response.getMsg())
);
还应保留完整的异常类型和堆栈,不能只查看 response.getMsg():
catch (AlipayApiException e) {
log.error(
"Alipay query exception: code={}, message={}",
e.getErrCode(),
e.getErrMsg(),
e
);
return AjaxResult.error("支付宝订单查询失败");
}
具体的异常访问方法可能随 SDK 版本变化,应以项目实际依赖提供的 API 为准。
是否需要更换 SDK 版本
根据上述代码,无法确认 4.40.308.ALL 存在必然导致该问题的兼容性缺陷,也无法据此给出一个保证能规避问题的版本号。
可以按以下顺序处理:
- 确认运行时实际加载的 SDK 版本。
- 检查依赖树中是否存在多个支付宝 SDK 版本。
- 查看当前版本的官方发布说明和已知问题。
- 在测试分支升级至官方当前支持的稳定版本。
- 使用同一笔沙盒订单和同一份配置,对比升级前后的结果。
- 如果输入完全相同,只有旧版本失败而新版本成功,再继续判断是否为 SDK 解析或验签兼容问题。
Maven 可以使用以下命令检查实际依赖。该命令用于排查,不要求在生产环境中执行:
mvn dependency:tree -Dincludes=com.alipay.sdk
还要检查打包产物中是否残留旧 JAR、容器镜像是否使用了缓存,以及其他依赖是否间接引入了不同版本。仅修改 pom.xml,不能证明运行时已经加载了新版本。
不要通过删除支付宝公钥、关闭响应验签,或捕获异常后直接认定查询成功来绕过问题。这些做法会失去对响应来源和内容完整性的校验。
如何定位响应原文问题
排查时应尽量取得以下信息,并对日志内容进行脱敏:
- 实际请求网关;
- 实际
app_id; charset;sign_type;- 支付宝公钥指纹;
- SDK 实际版本;
- HTTP 原始响应体;
- SDK 提取的
sign; - SDK 提取的待验签字符串;
- 完整的异常类型、错误码和堆栈。
最有价值的是未经反序列化和重新序列化的 HTTP 原始响应体。需要确认响应仍是标准结构:
{
"alipay_trade_query_response": {
"...": "..."
},
"sign": "..."
}
如果请求经过代理、网关、Mock 服务或抓包回放系统,还应检查这些中间环节是否进行了以下处理:
- 格式化或压缩 JSON;
- 调整字段顺序;
- 改写
/与\/; - 转换字符编码;
- 修改数字表示;
- 截断
sign; - 将错误页面包装为 JSON;
- 返回其他环境的缓存响应。
调试时可以对原始响应和待验签原文计算 SHA-256,避免在生产日志中长期记录订单详情和签名数据。
从沙盒切换到生产不能只换网关
业务调用代码通常不需要改变,但至少要重新核对以下配置和条件:
- 生产网关地址;
- 生产
appId; - 生产应用私钥;
- 生产支付宝公钥,或完整的生产证书链;
RSA2等签名类型;UTF-8等字符集;- 生产异步通知地址;
- 生产同步跳转地址;
- 应用已签约并开通电脑网站支付能力;
- 生产应用及相关能力已经审核、上线;
- 回调地址可以从公网访问;
- HTTPS 证书、域名、防火墙和反向代理配置正常;
- 金额、超时、订单号唯一性和幂等处理已经验证;
- 只有回调验签成功后才更新订单状态;
- 主动查询和异步通知的状态更新不会互相覆盖;
- 生产日志不会输出私钥、完整响应或用户敏感信息。
沙盒密钥、证书、应用 ID 和交易号不能在生产环境中复用。切换到生产环境后,还应完成小额真实支付、异步通知、主动查询、退款和重复通知等完整链路测试。
推荐排查顺序
- 记录当前环境、网关、
appId、signType、charset和支付宝公钥指纹。 - 确认查询客户端与回调验签使用同一套沙盒配置。
- 确认传入的是支付宝公钥,而不是应用公钥。
- 确认普通公钥模式和证书模式没有混用。
- 检查实际加载的 SDK 版本以及重复依赖。
- 保存一次脱敏后的原始响应,确认
sign没有被截断或改写。 - 排除代理、Mock、缓存和 JSON 二次序列化的影响。
- 使用官方当前支持的稳定 SDK,在相同输入下进行对照测试。
- 如果问题仍然存在,携带请求时间、接口名、
appId、支付宝请求标识和脱敏后的响应,联系支付宝技术支持核对服务端签名链路。
回调验签成功但查询响应验签失败时,最常见的原因仍是查询客户端使用了错误的支付宝公钥,或者混用了不同环境的配置,而不是 rsaCheck 与 rsaCheckV1 的方法差异。
备注:内容仅供参考。
