PAYATHON 2026

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 会按以下步骤处理:

  1. 排除 sign 和 sign_type。
  2. 按参数名排序。
  3. 拼接待验签字符串。
  4. 使用支付宝公钥验证 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 字节。如果验签时使用了不同长度或完全不匹配的公钥,就可能出现长度错误。

建议重点检查:

  1. 是否误将应用公钥用作支付宝公钥。
  2. 是否混用了沙盒和生产环境的支付宝公钥。
  3. 是否使用了另一个 appId 对应的支付宝公钥。
  4. 是否把证书内容错误地作为普通公钥传入。
  5. sign 是否被截断、添加了引号或重复进行 Base64 解码。
  6. 配置中心或环境变量是否覆盖了本地配置。
  7. 系统是否仍在复用旧的单例 AlipayClient。
  8. 是否有多个同名配置 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 存在必然导致该问题的兼容性缺陷,也无法据此给出一个保证能规避问题的版本号。

可以按以下顺序处理:

  1. 确认运行时实际加载的 SDK 版本。
  2. 检查依赖树中是否存在多个支付宝 SDK 版本。
  3. 查看当前版本的官方发布说明和已知问题。
  4. 在测试分支升级至官方当前支持的稳定版本。
  5. 使用同一笔沙盒订单和同一份配置,对比升级前后的结果。
  6. 如果输入完全相同,只有旧版本失败而新版本成功,再继续判断是否为 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 和交易号不能在生产环境中复用。切换到生产环境后,还应完成小额真实支付、异步通知、主动查询、退款和重复通知等完整链路测试。

推荐排查顺序

  1. 记录当前环境、网关、appId、signType、charset 和支付宝公钥指纹。
  2. 确认查询客户端与回调验签使用同一套沙盒配置。
  3. 确认传入的是支付宝公钥,而不是应用公钥。
  4. 确认普通公钥模式和证书模式没有混用。
  5. 检查实际加载的 SDK 版本以及重复依赖。
  6. 保存一次脱敏后的原始响应,确认 sign 没有被截断或改写。
  7. 排除代理、Mock、缓存和 JSON 二次序列化的影响。
  8. 使用官方当前支持的稳定 SDK,在相同输入下进行对照测试。
  9. 如果问题仍然存在,携带请求时间、接口名、appId、支付宝请求标识和脱敏后的响应,联系支付宝技术支持核对服务端签名链路。

回调验签成功但查询响应验签失败时,最常见的原因仍是查询客户端使用了错误的支付宝公钥,或者混用了不同环境的配置,而不是 rsaCheck 与 rsaCheckV1 的方法差异。

备注:内容仅供参考。