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 而失败。
排除路径后如果仍然报错,可以逐项检查:
- 支付平台实际发送的是不是真正的
GET,还是请求经过重定向后变成了POST。 - 反向代理、负载均衡器或其他 Filter 是否改写了请求方法或路径。
- 错误是否来自自定义的
csrfHeaderFilter(),而非 Spring Security 的标准CsrfFilter。 - 实际路径是否包含 context path,导致排除规则没有匹配。
- 应用中是否存在多条 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 防护范围。
备注:内容仅供参考。
