支付宝统一收单交易查询接口返回值如何验签?
结论
支付宝统一收单交易查询接口 alipay.trade.query 的同步返回值,需要使用支付宝公钥验签。
验签对象不是整个响应 JSON,也不是简单拼接 trade_no、out_trade_no 等字段,而是原始响应中 alipay_trade_query_response 节点对应的待验签内容。验签还需要用到响应中的 sign、接口配置的字符集和签名算法。只有验签通过,才能将返回的交易状态和金额用于后续业务处理。
典型响应结构如下:
{
"alipay_trade_query_response": {
"code": "10000",
"msg": "Success",
"trade_no": "2026xxxxxxxx",
"out_trade_no": "ORDER_10001",
"trade_status": "TRADE_SUCCESS"
},
"sign": "支付宝返回的签名值"
}
验签需要哪些参数
验签至少需要以下技术参数:
| 参数 | 用途 |
|---|---|
sign | 支付宝返回的签名值 |
| 待验签内容 | 原始响应中的 alipay_trade_query_response 节点内容 |
| 支付宝公钥 | 用于验证响应确实由支付宝签发 |
sign_type | 通常为 RSA2,必须与实际接入配置一致 |
charset | 通常为 UTF-8,必须与请求和响应处理方式一致 |
还要区分以下几种密钥:
- 应用私钥:商户向支付宝发送请求时,用于生成签名。
- 应用公钥:支付宝用它验证商户请求。
- 支付宝公钥:商户用它验证支付宝的同步响应或异步通知。
因此,不能使用应用公钥或应用私钥验证支付宝返回值。
如果采用支付宝公钥证书模式,应按照当前 SDK 的证书初始化方式配置支付宝公钥证书,不能直接沿用普通公钥模式的配置。
推荐做法:使用官方 SDK
使用支付宝官方 SDK 时,应在初始化客户端时正确配置支付宝公钥、字符集和签名算法,再通过 SDK 调用接口。不同语言、不同版本的 SDK 对同步响应的自动验签方式可能不同。有些版本在解析响应时完成验签,有些版本仍需调用单独的验签方法,因此应查看当前版本的说明。
Java 客户端的基本配置示例如下:
String serverUrl = "https://openapi.alipay.com/gateway.do";
String appId = "your_app_id";
String appPrivateKey = "your_app_private_key";
String format = "json";
String charset = "UTF-8";
String alipayPublicKey = "your_alipay_public_key";
String signType = "RSA2";
AlipayClient alipayClient = new DefaultAlipayClient(
serverUrl,
appId,
appPrivateKey,
format,
charset,
alipayPublicKey,
signType
);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(
"{\"out_trade_no\":\"ORDER_10001\"}"
);
AlipayTradeQueryResponse response = alipayClient.execute(request);
接口调用完成后,不能只根据 response.getCode() 或 response.isSuccess() 判断数据是否可信。还应确认当前 SDK 是否已经完成响应验签,并妥善处理验签失败时抛出的异常或返回的错误状态。
手动验签流程
如果没有使用 SDK 的自动验签功能,可以按以下流程处理。
1. 保留原始响应字符串
读取 HTTP 响应体后,先保留未经修改的原始字符串:
String responseBody = readOriginalResponseBody();
提取待验签内容之前,不要格式化响应,也不要调整字段顺序、转换字符转义或重新序列化。
2. 提取签名和待验签内容
从顶层响应中获取:
sign字段的值;alipay_trade_query_response节点对应的原始内容。
重点在于保留“原始内容”。下面的处理可能导致验签失败:
Object data = parseJson(responseBody);
String content = serializeJson(data);
JSON 经过解析和重新序列化后,字段顺序、空白字符、Unicode 转义或斜杠转义都可能发生变化,导致字节内容与支付宝签名时使用的内容不一致。
应优先使用支付宝 SDK 提供的响应解析和签名内容提取功能。具体方法名可能因 SDK 的语言和版本而异,不能只凭方法名推断参数格式。
3. 使用支付宝公钥验证签名
从密码学角度看,验签过程相当于:
Verify(
alipay_public_key,
response_content_bytes,
Base64Decode(sign),
signature_algorithm
)
sign_type 为 RSA2 时,通常对应 SHA256withRSA。历史系统如果配置为 RSA,通常对应 SHA1withRSA。实际算法必须以当前应用的接入配置为准,不能在代码中自行切换。
Java 伪代码如下:
String responseContent = extractOriginalResponseNode(
responseBody,
"alipay_trade_query_response"
);
String sign = extractTopLevelSign(responseBody);
boolean verified = verifyAlipaySignature(
responseContent,
sign,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
throw new SecurityException("支付宝交易查询响应验签失败");
}
extractOriginalResponseNode 和 verifyAlipaySignature 代表当前 SDK 或基础组件提供的功能。具体方法签名应以项目实际引入的 SDK 版本为准。
不依赖 SDK 的 RSA2 验签示例
如果已经可靠地取得原始待验签字符串,可以使用 JCA 验证 RSA2 签名:
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class AlipaySignatureVerifier {
public static boolean verifyRsa2(
String content,
String sign,
String alipayPublicKey,
String charset
) throws Exception {
String normalizedPublicKey = alipayPublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] publicKeyBytes = Base64.getDecoder()
.decode(normalizedPublicKey);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(content.getBytes(Charset.forName(charset)));
byte[] signatureBytes = Base64.getDecoder().decode(sign);
return verifier.verify(signatureBytes);
}
}
调用示例:
boolean verified = AlipaySignatureVerifier.verifyRsa2(
originalResponseContent,
responseSign,
alipayPublicKey,
"UTF-8"
);
if (!verified) {
throw new SecurityException("Invalid Alipay response signature");
}
这段代码只负责密码学验证,不能从完整 JSON 中准确提取原始待验签内容。生产环境仍建议使用官方 SDK,以免因 JSON 边界提取或转义处理不一致造成误判。
验签通过后的业务检查
验签成功只能说明响应在传输和处理过程中没有被篡改,并且签名可以通过对应的支付宝公钥验证。这并不代表交易一定成功。
验签通过后,还要继续检查:
if (!"10000".equals(response.getCode())) {
// 接口调用未成功,根据 code、subCode、subMsg 处理
}
if (!expectedOutTradeNo.equals(response.getOutTradeNo())) {
throw new SecurityException("商户订单号不匹配");
}
if (!expectedAmount.equals(response.getTotalAmount())) {
throw new SecurityException("订单金额不匹配");
}
String tradeStatus = response.getTradeStatus();
switch (tradeStatus) {
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
// 根据业务规则更新订单
break;
case "WAIT_BUYER_PAY":
// 等待付款
break;
case "TRADE_CLOSED":
// 交易关闭
break;
default:
// 未识别或暂不处理的状态
break;
}
比较金额时,建议使用 BigDecimal.compareTo(),不要使用浮点数:
BigDecimal expected = new BigDecimal("99.00");
BigDecimal actual = new BigDecimal(response.getTotalAmount());
if (expected.compareTo(actual) != 0) {
throw new SecurityException("交易金额不匹配");
}
常见验签失败原因
使用了错误的公钥
验签必须使用当前支付宝应用对应的支付宝公钥。沙箱环境与生产环境、普通公钥模式与证书模式的配置不能混用。
对整个响应体进行验签
顶层的 sign 字段不属于待验签业务内容。如果把包含 sign 在内的整个 JSON 用于验签,通常会失败。
JSON 被重新序列化
即使重新序列化后的 JSON 在语义上完全相同,只要字符表示发生变化,签名校验就可能失败。
字符集不一致
如果支付宝使用 UTF-8 签名,而本地使用系统默认字符集转换字节,验签结果就会不一致。代码中应显式指定字符集。
签名算法不一致
接口配置为 RSA2,本地却使用 SHA1withRSA,或者接口配置为 RSA,本地却使用 SHA256withRSA,都会导致验签失败。
公钥格式不正确
普通支付宝公钥一般按 X.509 格式解析。复制公钥时缺行、混入多余字符或全角空格,或者把证书内容当成普通公钥使用,都可能造成解析或验签失败。
处理原则
生产代码应按以下顺序处理:
读取原始响应
→ 提取原始待验签内容和 sign
→ 使用支付宝公钥验签
→ 验签失败立即拒绝处理
→ 检查接口返回 code
→ 核对订单号、金额等业务字段
→ 根据 trade_status 更新订单
验签失败后,不要继续使用响应中的交易状态,也不要为了“兼容”而跳过验签。如果官方 SDK 的方法名、自动验签行为或证书配置方式与示例不同,应以项目实际使用的 SDK 版本为准。但无论使用哪个版本,都不能混淆待验签内容、公钥用途和业务校验顺序。
备注:内容仅供参考。
