PAYATHON 2026

Spring Security 支付回调提示 Expected CSRF token not found

支付小周

结论

permitAll() 只表示访问回调地址不需要登录,请求仍会经过 Spring Security 的 CSRF 过滤器。

支付平台从服务器发起请求时,通常不会携带浏览器中的 Session、CSRF Cookie 或自定义 CSRF Header。浏览器直接访问同一地址时,会自动带上已有的 Cookie,因此两种访问方式可能得到不同结果。

需要将支付回调路径排除在 CSRF 校验之外,同时检查自定义的 csrfHeaderFilter(),确保它不会再次要求该路径提供 Token。

推荐配置

只排除具体的回调地址,不要关闭整个站点的 CSRF 防护:

http
    .httpBasic()
    .and()
    .authorizeRequests()
        .antMatchers("/p/paysera/callback").permitAll()
        .anyRequest().authenticated()
    .and()
    .csrf()
        .csrfTokenRepository(csrfTokenRepository())
        .ignoringAntMatchers("/p/paysera/callback")
    .and()
    .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);

Spring Security 4.0 支持通过 ignoringAntMatchers(...) 跳过指定路径的 CSRF 校验。

下面这项配置:

.antMatchers("/p/**").permitAll()

只控制授权判断,不会让请求跳过安全过滤器链。CsrfFilter 和自行添加的过滤器仍然会处理该请求。

检查自定义 CSRF Filter

当前配置还注册了以下过滤器:

.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)

如果这个过滤器会主动检查 CSRF Token,即使标准 CsrfFilter 已经忽略回调路径,它仍可能拒绝请求。可以让它跳过该回调地址,例如使用 OncePerRequestFilter:

public class CsrfHeaderFilter extends OncePerRequestFilter {

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return "/p/paysera/callback".equals(request.getServletPath());
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {

        // 原有 CSRF Header/Cookie 处理逻辑
        filterChain.doFilter(request, response);
    }
}

如果该过滤器只负责把 CSRF Token 写入响应 Cookie,而不验证请求,也要注意不要在 Token 不存在时直接抛出异常。

为什么 GET 请求仍可能报 CSRF 错误

Spring Security 默认不会校验 GET、HEAD、TRACE 和 OPTIONS 请求的 CSRF Token。因此,真实的 GET 请求经过标准 CsrfFilter 时,通常不会仅因缺少 Token 而失败。

排除路径后如果仍然报错,可以逐项检查:

  1. 支付平台实际发送的是不是真正的 GET,还是请求经过重定向后变成了 POST。
  2. 反向代理、负载均衡器或其他 Filter 是否改写了请求方法或路径。
  3. 错误是否来自自定义的 csrfHeaderFilter(),而非 Spring Security 的标准 CsrfFilter。
  4. 实际路径是否包含 context path,导致排除规则没有匹配。
  5. 应用中是否存在多条 Spring Security Filter Chain,请求是否进入了另一条配置。

可以临时记录请求信息进行确认:

logger.info(
    "method={}, requestURI={}, servletPath={}",
    request.getMethod(),
    request.getRequestURI(),
    request.getServletPath()
);

Controller 示例

回调处理成功后,直接返回支付平台要求的纯文本:

@RequestMapping(
    value = "/p/paysera/callback",
    method = RequestMethod.GET,
    produces = "text/plain"
)
@ResponseBody
public String payseraCallback(
        @RequestParam("data") String data,
        @RequestParam("ss1") String ss1) {

    // 验证签名、解析数据并以幂等方式处理订单
    return "OK";
}

安全注意事项

排除 CSRF 校验并不代表可以信任回调请求。CSRF Token 主要用于保护依赖浏览器 Session 的操作,而第三方服务器无法预先取得用户 Session 中的 Token。支付回调通常应通过支付平台提供的签名机制验证。

回调处理至少应满足以下要求:

  • 严格校验 data、ss1 等签名参数。
  • 校验商户、订单号、金额和币种。
  • 保证处理逻辑幂等,防止重复回调多次修改订单。
  • 不要因为请求访问了 /p/paysera/callback 就认定支付成功。
  • 只排除具体的回调地址,不要将整个 /p/** 移出 CSRF 防护范围。

备注:内容仅供参考。