PAYATHON 2026

springboot java开发中支付宝异步通知参数为空如何解决

支付小周

结论

支付宝异步通知通常通过 POST 请求发送,参数格式一般为 application/x-www-form-urlencoded。如果请求确实来自支付宝,请求体没有被提前读取、替换或丢弃,并且 Content-Type 正确,request.getParameterMap() 应该能正常取得参数。

接口可以进入,但参数始终为空,通常需要排查以下情况:

  1. 收到的并不是支付宝正式通知,而是浏览器、监控程序或网关发起的探活请求。
  2. 请求的 Content-Type 不正确,实际内容是 JSON、纯文本,或者没有声明类型。
  3. Filter、Interceptor、网关或自定义 HttpServletRequestWrapper 提前读取或替换了请求体。
  4. 反向代理、API 网关或转发服务没有完整转发 POST 请求体。
  5. 参数读取工具有问题,或者请求在读取参数前被错误包装。
  6. 通知地址、运行环境或路由配置有误,日志中的请求来自其他调用方。

先记录请求的基础信息

不要只记录参数。先确认应用实际收到了什么请求:

@PostMapping(
    value = "/pay/notice",
    produces = "text/plain;charset=UTF-8"
)
public String payNotice(HttpServletRequest request) {
    log.info(
        "支付宝通知请求:method={}, contentType={}, contentLength={}, queryString={}, remoteAddr={}, userAgent={}",
        request.getMethod(),
        request.getContentType(),
        request.getContentLengthLong(),
        request.getQueryString(),
        request.getRemoteAddr(),
        request.getHeader("User-Agent")
    );

    Map<String, String[]> parameterMap = request.getParameterMap();
    log.info("支付宝通知参数:{}", JSON.toJSONString(parameterMap));

    return "success";
}

需要重点确认:

  • method 是否为 POST。
  • contentType 是否类似 application/x-www-form-urlencoded。
  • contentLength 是否大于 0。
  • 当前请求路径是否就是配置给支付宝的异步通知地址。
  • 是否存在重复请求,其中一部分只是探活或人工测试。
  • 请求是否经过 Nginx、Ingress、WAF、API 网关或其他转发服务。

如果 contentLength 为 0,问题通常在调用方或中间转发层。如果请求体有长度但参数为空,应重点检查 Content-Type,以及请求体是否被包装或提前读取。

按支付宝表单通知方式接收

表单参数可以直接通过 @RequestParam 接收:

@PostMapping(
    value = "/pay/notice",
    consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
    produces = "text/plain;charset=UTF-8"
)
public String payNotice(@RequestParam Map<String, String> params) {
    log.info("支付宝异步通知参数:{}", JSON.toJSONString(params));

    // 1. 验签
    // 2. 校验 app_id、seller_id、订单号、金额等业务字段
    // 3. 幂等更新订单
    // 4. 全部处理成功后返回 success

    return "success";
}

也可以从 HttpServletRequest 中转换参数。考虑到同名参数可能出现多次,遍历 String[] 会更稳妥:

private Map<String, String> getAlipayParams(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((name, values) -> {
        String value = values == null
            ? ""
            : String.join(",", values);
        params.put(name, value);
    });

    return params;
}

Controller 示例:

@PostMapping(
    value = "/pay/notice",
    produces = "text/plain;charset=UTF-8"
)
public String payNotice(HttpServletRequest request) {
    Map<String, String> params = getAlipayParams(request);
    log.info("支付宝异步通知参数:{}", JSON.toJSONString(params));

    if (params.isEmpty()) {
        log.warn(
            "支付宝通知参数为空:contentType={}, contentLength={}",
            request.getContentType(),
            request.getContentLengthLong()
        );
        return "failure";
    }

    // 验签及业务处理
    return "success";
}

检查是否错误地发送了 JSON

request.getParameterMap() 读取的是查询参数,以及 Servlet 能够解析的表单参数。它不会自动把 JSON 请求体转换成参数。

例如,下面测试请求中的内容不会出现在 getParameterMap() 中:

Content-Type: application/json

{"out_trade_no":"202609090001","trade_status":"TRADE_SUCCESS"}

如果自建中转服务使用 JSON 转发,就需要按 JSON 接收:

@PostMapping(
    value = "/pay/notice",
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = "text/plain;charset=UTF-8"
)
public String payNotice(@RequestBody Map<String, Object> body) {
    log.info("通知 JSON:{}", JSON.toJSONString(body));
    return "success";
}

但不能因为参数为空,就直接把支付宝回调接口改为只接收 JSON。应先查看实际请求的 Content-Type。如果支付宝通知经过自建网关转发,建议让网关保留原始表单格式和参数值,以免影响验签。

检查请求体是否被提前读取

如果项目中使用了以下组件,需要逐一检查:

  • 请求日志 Filter。
  • XSS 过滤器。
  • 解密或验签 Filter。
  • 自定义 HttpServletRequestWrapper。
  • 全局审计组件。
  • API 网关请求改写插件。

常见错误是 Filter 先调用 request.getInputStream() 或 request.getReader() 读取请求体,随后又向下传递了一个无法重复读取,或没有完整实现参数解析的包装对象。

String body = request.getReader()
    .lines()
    .collect(Collectors.joining());

不要在支付宝通知到达 Controller 前随意读取原始请求体。如果日志或审计组件必须读取,应使用能够缓存请求内容的包装方案,并确认包装类正确实现以下方法:

  • getInputStream()
  • getReader()
  • getParameter()
  • getParameterMap()
  • getParameterValues()

还需要注意,ContentCachingRequestWrapper 的主要作用是缓存已经读取的内容,并不保证在任意读取顺序下都能自动恢复 Servlet 表单参数。支付宝验签时,直接使用 Servlet 解析后的参数集合仍然更稳妥。

检查字符编码设置顺序

字符编码必须在第一次读取参数之前设置。可以通过全局 Filter 统一配置:

@Bean
public FilterRegistrationBean<CharacterEncodingFilter> characterEncodingFilter() {
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding(StandardCharsets.UTF_8.name());
    filter.setForceEncoding(true);

    FilterRegistrationBean<CharacterEncodingFilter> bean =
        new FilterRegistrationBean<>(filter);
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

编码配置错误通常会造成中文乱码,不一定会让整个参数集合变为空。不过,如果项目中有自定义参数解析或请求包装逻辑,仍然需要检查编码的设置顺序。

使用表单请求复现

可以先绕过支付宝,直接测试应用能否解析表单参数:

curl -i -X POST 'https://example.com/pay/notice' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'out_trade_no=202609090001' \
  --data-urlencode 'trade_status=TRADE_SUCCESS' \
  --data-urlencode 'total_amount=100.00'

如果此时可以取得参数,说明 Spring MVC 和 Controller 基本正常。接下来应检查支付宝后台配置的通知地址和中间代理。

如果直连应用时正常,经过域名或网关后参数为空,需要检查代理是否完整保留了:

  • POST 方法。
  • 原始请求体。
  • Content-Type 请求头。
  • Content-Length 或分块传输。
  • URL 编码后的表单内容。

如果本地 curl 测试仍然取不到参数,可以暂时停用自定义 Filter、Interceptor 和 Wrapper,再逐个恢复,定位是哪个组件造成了问题。

不要在读取参数前后混用请求体

处理表单请求时,不建议同时使用下面两种读取方式:

request.getParameterMap();
request.getInputStream();

Servlet 容器解析表单参数时可能会读取请求体。受容器、包装器和调用顺序影响,混用这两种方式可能造成原始流为空,或者参数无法解析。确定使用表单参数验签后,应统一从 getParameterMap() 或 @RequestParam 获取数据。

支付通知处理还需要完成验签和业务校验

参数不为空,并不代表通知可信。处理异步通知时,至少要完成验签:

Map<String, String> params = getAlipayParams(request);

boolean signVerified = AlipaySignature.rsaCheckV1(
    params,
    alipayPublicKey,
    StandardCharsets.UTF_8.name(),
    signType
);

if (!signVerified) {
    log.warn("支付宝异步通知验签失败");
    return "failure";
}

具体的方法签名取决于项目使用的支付宝 SDK 版本,应以当前依赖版本提供的 API 为准。验签之外,还要校验:

  • app_id 是否属于当前应用。
  • seller_id 是否属于预期收款方。
  • out_trade_no 是否存在并属于当前系统。
  • total_amount 是否与本地订单金额一致。
  • trade_status 是否属于业务允许处理的状态。
  • 同一通知重复到达时,订单处理是否具备幂等性。

只有验签、字段校验和订单处理都成功后,才能返回以下纯文本:

success

参数为空、验签失败或关键业务处理失败时,不要返回 success。否则支付宝可能认为通知已经处理成功,并停止重试。生产日志也不应长期完整记录所有回调字段,可能包含敏感信息的内容需要脱敏。

建议先确认请求来源和 Content-Type,再用 curl 验证表单解析,随后绕过代理进行对比,最后检查 Filter 和自定义请求包装器。只要真实请求保持 application/x-www-form-urlencoded 格式,并且请求体没有被中间层丢弃,request.getParameterMap() 通常可以正常取得支付宝通知参数。

备注:内容仅供参考。