支付宝网站支付同步回调使用 rsaCheckV1 验签失败
结论
sign 和 sign_type 应从支付宝跳转到 return_url 时携带的原始 HTTP 请求参数中获取,不能从支付接口返回的 AlipayTradePagePayResponse、业务对象或手工筛选后的参数集合中获取。
你提供的参数集合中没有 sign,因此无法通过 AlipaySignature.rsaCheckV1 验签。签名值也不能根据其他参数重新计算或补全。需要检查参数获取、框架绑定、重定向和网关转发过程。如果最终收到的请求中确实没有 sign,就必须按验签失败处理。
为什么参数中没有 sign
支付宝正常访问 return_url 时,会同时携带业务参数和以下两个参数:
sign
sign_type
签名参数缺失,常见原因有:
- 获取的不是
return_url对应的原始请求参数,而是支付接口响应对象中的字段。 - Controller 通过 DTO、实体类或固定参数列表接收回调,但对象中没有定义
sign和sign_type。 - 构造验签参数 Map 时,只复制了部分业务字段。
- 前端页面、反向代理或中间跳转地址重新拼接 URL 时遗漏了参数。
- 输出日志前,代码已经删除了
sign和sign_type。 - 代码读取了请求体,而同步跳转参数实际位于 URL 查询字符串中。
可以先查看浏览器地址栏、开发者工具,或服务端记录的原始 queryString,确认支付宝发起的跳转请求是否包含签名参数。
正确获取同步回调参数
在 Java Servlet 或 Spring MVC 中,应通过 HttpServletRequest.getParameterMap() 获取完整参数:
import com.alipay.api.internal.util.AlipaySignature;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public boolean verifyReturn(HttpServletRequest request) 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();
StringBuilder value = new StringBuilder();
for (int i = 0; i < values.length; i++) {
if (i > 0) {
value.append(",");
}
value.append(values[i]);
}
params.put(entry.getKey(), value.toString());
}
String sign = params.get("sign");
String signType = params.get("sign_type");
if (sign == null || sign.isEmpty()) {
return false;
}
if (signType == null || signType.isEmpty()) {
return false;
}
return AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
signType
);
}
这里的:
ALIPAY_PUBLIC_KEY
必须是支付宝公钥,不能使用应用公钥或应用私钥。使用支付宝开放平台公钥模式时,应填写当前应用对应的支付宝公钥。使用证书模式时,则要采用证书模式对应的 SDK 验签方法和配置,不能直接沿用普通公钥模式的代码。
Spring MVC 的处理方法可以这样写:
@GetMapping("/alipay/return")
public String alipayReturn(HttpServletRequest request) {
try {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
params.put(name, String.join(",", values));
});
String sign = params.get("sign");
String signType = params.get("sign_type");
if (sign == null || sign.isEmpty()
|| signType == null || signType.isEmpty()) {
return "pay/verify-failed";
}
boolean verified = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
signType
);
if (!verified) {
return "pay/verify-failed";
}
// 验签通过后,还应核对 app_id、out_trade_no、total_amount、
// seller_id 等参数是否与本地订单一致。
return "pay/success";
} catch (Exception e) {
return "pay/verify-failed";
}
}
不建议只在 Controller 中声明部分参数,例如:
@GetMapping("/alipay/return")
public String alipayReturn(
String out_trade_no,
String trade_no,
String total_amount) {
// 这里没有取得完整回调参数,无法正确验签。
return "pay/success";
}
也不要先把请求转换为只含业务字段的对象,再用该对象生成验签 Map。
没有 sign 时不能验签
rsaCheckV1 会使用支付宝公钥验证支付宝提供的数字签名。完成验证至少需要:
- 支付宝返回的完整待验签参数;
- 支付宝返回的
sign; - 正确的支付宝公钥;
- 与回调一致的字符集;
- 正确的签名类型,通常为
RSA2。
没有 sign,就没有可供验证的数字签名。因此,下面这种做法没有意义:
params.put("sign", "自行生成的签名");
应用私钥生成的是应用方签名,无法代替支付宝为同步回调参数生成的签名。订单号、交易号或金额也不足以证明这次跳转可信。
排查方法
排查时,可以临时记录参数名称和原始查询字符串:
String queryString = request.getQueryString();
request.getParameterMap().forEach((name, values) -> {
System.out.println(name + "=" + String.join(",", values));
});
生产环境中不要长期打印完整签名、用户信息或其他敏感参数。
如果 queryString 中有 sign,但业务 Map 中没有,问题通常出在参数转换、DTO 绑定或字段过滤环节。如果 queryString 本身就没有 sign,需要继续检查:
- 当前请求是否确实由支付宝直接跳转而来;
return_url是否又经过了一层前端或服务端重定向;- 中间页面拼接 URL 时是否只保留了部分参数;
- 网关或代理是否设置了查询参数白名单;
- 测试时是否手工复制了不完整的回调 URL。
注意事项
同步回调依赖用户浏览器完成跳转。用户可能关闭页面、遇到断网,或者阻止跳转,所以不能把同步回调作为更新订单支付状态的唯一依据。
订单最终状态应以支付宝异步通知 notify_url 的验签结果为主,必要时再主动调用交易查询接口确认。同步回调主要用于展示支付结果页面。
即使同步回调已经通过验签,仍要核对以下字段:
app_id
seller_id
out_trade_no
total_amount
这些字段必须与当前应用、收款账号和本地订单一致。不能因为页面收到了 trade_no,就直接把订单标记为支付成功。
备注:内容仅供参考。
