支付宝支付回调 RSA2 验签提示待验签内容为空的原因
结论
是的。异常中的 content= 和 sign=null 说明,调用 RSA2 验签时,传入的待验签内容是空的,同时也没有取到支付宝回调参数中的 sign。此时验签还没有进入公钥计算阶段,应先检查回调参数的接收和组装逻辑,不必急着更换公钥。
常见原因有:
- Controller 没有通过
HttpServletRequest#getParameterMap()获取表单参数。 - 使用
@RequestBody,把支付宝异步通知当成 JSON 接收。 - 请求体已被过滤器、日志组件或网关读取,后续组件无法再次读取。
- 参数 Map 在验签前被清空、转换错误,或者漏掉了
sign。 - 收到的请求并非支付宝通知,例如回调地址被浏览器、监控服务或其他程序访问。
- 使用个别字段或业务对象重新拼装验签内容,导致参数不完整。
SDK 版本 4.39.165.ALL 本身无法解释 content= 和 sign=null。只要传给 SDK 的参数为空,就可能出现这类异常。
正确获取回调参数
支付宝支付异步通知通常通过 HTTP 表单参数提交。Java Web 项目应从 request.getParameterMap() 获取全部参数,再交给 SDK 验签。
import com.alipay.api.internal.util.AlipaySignature;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public boolean verifyAlipayNotify(
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();
if (values == null || values.length == 0) {
params.put(entry.getKey(), "");
} else {
params.put(entry.getKey(), String.join(",", values));
}
}
String sign = params.get("sign");
if (params.isEmpty() || sign == null || sign.isBlank()) {
throw new IllegalArgumentException(
"支付宝回调参数为空或缺少 sign,无法执行验签");
}
return AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
}
使用 Spring MVC 时,可以在回调方法中直接接收 HttpServletRequest:
@PostMapping(
value = "/alipay/notify",
produces = "text/plain;charset=UTF-8"
)
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
try {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
String value = values == null
? ""
: String.join(",", values);
params.put(name, value);
});
String sign = params.get("sign");
if (params.isEmpty() || sign == null || sign.isBlank()) {
// 记录参数名、Content-Type、请求方法等排查信息,
// 不要在生产日志中完整输出敏感业务数据。
return "failure";
}
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
// 验签通过后继续校验业务字段,并以幂等方式处理订单。
// processNotify(params);
return "success";
} catch (Exception e) {
// 记录异常并告警
return "failure";
}
}
不要把支付宝异步通知直接声明为 JSON:
// 不建议:支付宝异步通知通常不是 JSON 请求体
@PostMapping("/alipay/notify")
public String notify(@RequestBody AlipayNotifyDTO body) {
// ...
}
如果项目使用 Spring Boot 3 或 Jakarta Servlet,需要将示例中的 javax.servlet.http.HttpServletRequest 替换为:
import jakarta.servlet.http.HttpServletRequest;
重点排查步骤
1. 确认回调请求已经到达应用
进入验签前,记录以下信息:
String method = request.getMethod();
String contentType = request.getContentType();
int parameterCount = request.getParameterMap().size();
String sign = request.getParameter("sign");
String tradeStatus = request.getParameter("trade_status");
String outTradeNo = request.getParameter("out_trade_no");
检查以下内容:
- 请求方法是否为
POST。 Content-Type能否被 Servlet 容器解析为表单参数。parameterCount是否大于零。- 参数中是否包含
sign、sign_type、out_trade_no等字段。 - 反向代理是否重定向或改写了回调地址。
日志可以记录参数名和关键订单号,但不要长期完整打印所有支付数据和签名内容。
2. 检查请求体是否被错误读取
如果代码先调用了以下方法:
request.getInputStream();
request.getReader();
之后又由其他组件解析请求体,就可能取不到参数。需要重点检查:
- 请求日志过滤器。
- 签名过滤器。
- API 网关插件。
- Spring
HandlerInterceptor。 - 自定义
HttpServletRequestWrapper。
如果确实需要多次读取请求体,应使用能够缓存请求内容的包装器,同时确保表单参数解析不受影响。支付宝回调通常直接读取 getParameterMap() 即可,无需手动读取原始请求体。
3. 检查参数 Map 的构造过程
不要只放入业务处理所需的几个字段:
Map<String, String> params = new HashMap<>();
params.put("out_trade_no", request.getParameter("out_trade_no"));
params.put("trade_status", request.getParameter("trade_status"));
RSA2 签名覆盖支付宝通知中的签名相关参数集合。字段一旦遗漏,即使最终生成了非空的待验签字符串,验签仍会失败。
验签前也不要修改字段值,例如:
- 对参数值调用
trim()。 - 将空字符串改为
null。 - 把金额转换为数值后重新格式化。
- 对时间、中文或特殊字符重新编码。
- 对参数进行二次 URL 解码。
- 使用 DTO 序列化后的字段验签。
从请求中取得的原始参数值应直接传给 AlipaySignature.rsaCheckV1。参数排序以及排除 sign、sign_type 等工作由 SDK 完成。
4. 确认支付宝公钥配置正确
参数能够正常获取,但验签结果仍为 false 时,再检查公钥配置:
- 使用支付宝公钥,不要使用应用公钥。
- 公钥属于当前应用和当前环境。
- 没有混用沙箱环境与正式环境的密钥。
sign_type为RSA2时,验签参数也使用"RSA2"。- 字符集与支付请求配置一致,通常为
UTF-8。 - 公钥字符串没有缺失内容、换行污染或多余空格。
如果应用使用支付宝公钥证书模式,应通过证书模式对应的 SDK 接口和配置完成验签,不能将证书内容直接当成普通支付宝公钥字符串传入。
为什么会出现 content= 和 sign=null
SDK 验签需要两个主要输入:
- 根据回调参数构造的待验签字符串
content。 - 支付宝发送的签名字段
sign。
异常已经打印了实际输入:
content=
sign=null
这表示传入验签方法的参数集合为空,或者代码调用了较底层的验签方法,并显式传入了空字符串和 null。在这种情况下,即使公钥完全正确,也无法完成验签。
如果问题出在公钥不匹配,通常会先取得非空的待验签内容和签名,随后出现验签失败或密钥解析异常,而不是当前这种“待验签内容不可为空”的错误。
验签通过后还要校验业务字段
RSA2 验签通过,只能说明通知通过了签名校验,不能直接认定订单可以入账。至少还要核对以下字段:
String appId = params.get("app_id");
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
String sellerId = params.get("seller_id");
应结合自身交易数据确认:
app_id是否属于当前应用。seller_id是否为预期的支付宝账号。out_trade_no是否存在并属于当前系统。total_amount是否与订单金额一致。trade_status是否属于需要处理的成功状态。- 同一通知重复到达时,订单处理是否具备幂等性。
只有验签、业务字段校验和订单处理全部成功后,才返回以下纯文本:
success
处理失败时返回:
failure
不要返回 JSON、HTML 或包含额外说明的字符串,否则支付宝可能无法识别成功响应,并继续重试通知。
备注:内容仅供参考。
