PAYATHON 2026

支付回调异步验签调用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,但请求没有进入异步模式,或者异步处理已经结束。

原因分析

常见原因包括:

  1. 调用了 request.getAsyncContext(),此前却没有调用 request.startAsync()。
  2. 请求曾经进入异步模式,但已经执行过 complete(),之后又访问了 AsyncContext。
  3. 异步线程继续使用原始 HttpServletRequest,而请求生命周期已经结束。
  4. Filter 或 Servlet 没有启用异步支持,却尝试启动异步处理。
  5. 全局 Filter、日志组件、请求包装器或异常处理代码错误地把支付回调当作异步 Servlet 请求处理。
  6. 验签附近的其他代码抛出了异常,调用位置看起来像是 rsaCheckV1 报错。

先查看完整的异常堆栈,找到第一个业务代码调用点,并重点检查是否调用了以下方法:

request.getAsyncContext();
request.startAsync();
asyncContext.complete();
asyncContext.dispatch();

如果异常堆栈没有进入 AlipaySignature 的验签逻辑,基本可以确认问题出在 Web 请求生命周期管理,而不是 RSA 签名计算。

推荐解决方案

支付平台所说的“异步通知”,是指支付宝服务器主动回调商户接口,并不等同于 Servlet 规范中的异步请求模式。大多数支付回调接口采用同步处理即可:

  1. 从 HttpServletRequest 中读取全部回调参数。
  2. 调用 AlipaySignature.rsaCheckV1 验签。
  3. 校验订单号、商户身份、金额和交易状态。
  4. 幂等更新订单。
  5. 返回支付宝要求的成功响应。

不要因为接口被称为“异步回调”,就调用 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。否则应按照支付宝接口约定返回失败结果。
  • 不要将公钥、私钥、完整回调报文或用户敏感信息直接写入公开日志。

备注:内容仅供参考。