支付回调异步验签调用AlipaySignature.rsaCheckV1报错
结论
这个异常通常不是 AlipaySignature.rsaCheckV1 验签失败引起的,而是 Servlet 异步请求状态使用不当。
rsaCheckV1 只负责根据回调参数、支付宝公钥、字符集和签名算法完成验签,不需要调用 request.startAsync()。异常信息如下:
It is illegal to call this method if the current request is not in asynchronous mode
(i.e. isAsyncStarted() returns false)
这说明当前调用链中的 Controller、Filter、Interceptor、请求包装器或异常处理器调用了 request.getAsyncContext()、AsyncContext.complete() 等异步 API,但请求没有进入异步模式,或者异步处理已经结束。
原因分析
常见原因包括:
- 调用了
request.getAsyncContext(),此前却没有调用request.startAsync()。 - 请求曾经进入异步模式,但已经执行过
complete(),之后又访问了AsyncContext。 - 异步线程继续使用原始
HttpServletRequest,而请求生命周期已经结束。 - Filter 或 Servlet 没有启用异步支持,却尝试启动异步处理。
- 全局 Filter、日志组件、请求包装器或异常处理代码错误地把支付回调当作异步 Servlet 请求处理。
- 验签附近的其他代码抛出了异常,调用位置看起来像是
rsaCheckV1报错。
先查看完整的异常堆栈,找到第一个业务代码调用点,并重点检查是否调用了以下方法:
request.getAsyncContext();
request.startAsync();
asyncContext.complete();
asyncContext.dispatch();
如果异常堆栈没有进入 AlipaySignature 的验签逻辑,基本可以确认问题出在 Web 请求生命周期管理,而不是 RSA 签名计算。
推荐解决方案
支付平台所说的“异步通知”,是指支付宝服务器主动回调商户接口,并不等同于 Servlet 规范中的异步请求模式。大多数支付回调接口采用同步处理即可:
- 从
HttpServletRequest中读取全部回调参数。 - 调用
AlipaySignature.rsaCheckV1验签。 - 校验订单号、商户身份、金额和交易状态。
- 幂等更新订单。
- 返回支付宝要求的成功响应。
不要因为接口被称为“异步回调”,就调用 request.getAsyncContext() 或 request.startAsync()。
同步验签示例
@PostMapping("/alipay/notify")
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
if (values != null && values.length > 0) {
params.put(name, String.join(",", values));
}
});
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
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");
// 继续校验 app_id、seller_id、订单金额和订单状态。
// 使用 out_trade_no 或 trade_no 做幂等处理,避免重复更新订单。
processPaymentNotify(outTradeNo, tradeNo, tradeStatus, totalAmount);
return "success";
} catch (AlipayApiException e) {
// 记录必要的异常信息,但不要输出私钥、完整公钥等敏感数据。
return "failure";
}
}
不同版本的支付宝 SDK 可能提供不同形式的 rsaCheckV1 参数,应以项目当前依赖中的方法签名为准。
如果业务确实需要 Servlet 异步处理
业务确实需要使用 Servlet 异步能力时,应显式启动异步模式:
@WebServlet(value = "/alipay/notify", asyncSupported = true)
public class AlipayNotifyServlet extends HttpServlet {
@Override
protected void doPost(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((name, values) -> {
if (values != null && values.length > 0) {
params.put(name, String.join(",", values));
}
});
AsyncContext asyncContext = request.startAsync();
asyncContext.start(() -> {
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("text/plain;charset=UTF-8");
asyncResponse.getWriter().write(verified ? "success" : "failure");
} catch (Exception e) {
try {
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.getWriter().write("failure");
} catch (IOException ignored) {
// 记录日志
}
} finally {
asyncContext.complete();
}
});
}
}
同时还要确保请求经过的所有 Filter 均支持异步:
@WebFilter(value = "/*", asyncSupported = true)
public class RequestFilter implements Filter {
// ...
}
支付回调通常不适合长时间占用异步处理。更稳妥的做法是先完成验签和必要的订单落库,再将耗时业务投递到消息队列,并尽快返回 success。
Spring MVC 项目的排查重点
如果 Controller 没有直接使用 Servlet 异步 API,还需要检查:
- 自定义
Filter是否调用了getAsyncContext()。 - 日志或链路追踪组件是否在请求结束后读取异步上下文。
OncePerRequestFilter是否正确处理异步分发。- 是否混用
Callable、DeferredResult、WebAsyncTask和原始HttpServletRequest。 - 异步线程是否持有并访问已经失效的 request 或 response。
- 是否重复调用了
AsyncContext.complete()。 - 容器与中间件版本之间是否存在兼容问题。
排查时可以在相关位置临时记录请求状态:
log.info(
"asyncSupported={}, asyncStarted={}",
request.isAsyncSupported(),
request.isAsyncStarted()
);
只有当 request.isAsyncStarted() 返回 true 时,才能安全调用:
AsyncContext asyncContext = request.getAsyncContext();
不过,不要只用条件判断绕过异常。如果业务不需要 Servlet 异步处理,应直接删除相关的异步上下文调用。
验签注意事项
alipayPublicKey应使用支付宝公钥,不能使用应用私钥或应用公钥。sign_type通常是RSA2,具体应以实际回调参数和支付宝应用配置为准。- 字符集必须与回调配置一致,常见值是
UTF-8。 - 应将回调的全部原始参数交给 SDK,由 SDK 按规则处理
sign和sign_type。 - 验签前不要自行修改参数值、重新编码或进行不必要的 URL 解码。
- 验签通过不代表业务数据一定有效,还要核对
app_id、商户身份、订单金额、订单号和交易状态。 - 支付回调可能重复发送,因此订单处理必须具备幂等性。
- 只有验签和业务处理都成功后,才能返回
success。否则应按照支付宝接口约定返回失败结果。 - 不要将公钥、私钥、完整回调报文或用户敏感信息直接写入公开日志。
备注:内容仅供参考。
