CsrfFilter
定义与作用
CsrfFilter 是 Spring Security 过滤器链中负责 CSRF Token 生成与校验 的核心过滤器。它位于 LogoutFilter 之后、UsernamePasswordAuthenticationFilter 之前,通过要求客户端在每个状态变更请求中携带有效的 CSRF Token,防止跨站请求伪造攻击。本章聚焦其作为过滤器的工作机制,CSRF 攻击原理在第八章详讲。
核心原理
1. CsrfTokenRepository:Token 存储抽象
CsrfFilter 通过 CsrfTokenRepository 接口与存储层交互,实现 Token 的持久化和读取:
public interface CsrfTokenRepository {
// 生成新的 CSRF Token
CsrfToken generateToken(HttpServletRequest request);
// 保存 Token(通常存入 Session 或 Cookie)
void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response);
// 加载当前 Token
CsrfToken loadToken(HttpServletRequest request);
}
Spring Security 5.x 提供三种实现:
| 实现类 | 存储位置 | 适用场景 |
|---|---|---|
HttpSessionCsrfTokenRepository(默认) | HttpSession | 传统服务端渲染应用(Thymeleaf / JSP) |
CookieCsrfTokenRepository | Cookie(XSRF-TOKEN) | SPA 前端应用(Angular / React / Vue) |
LazyCsrfTokenRepository | 延迟包装(装饰器) | 延迟生成,减少不必要的 Token 创建 |
2. 请求时序与 Token 流转
3. Token 校验核心逻辑
// CsrfFilter 核心逻辑(简化)
public void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
// 1. 延迟加载/生成 Token(确保 Token 始终可用)
CsrfToken csrfToken = tokenRepository.loadToken(request);
if (csrfToken == null) {
csrfToken = tokenRepository.generateToken(request);
tokenRepository.saveToken(csrfToken, request, response);
}
// 2. 将 Token 绑定到请求属性,供后续使用(如表单渲染)
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 3. 判断请求是否需要校验(GET/HEAD/TRACE/OPTIONS 跳过)
if (!requireCsrfProtectionMatcher.matches(request)) {
chain.doFilter(request, response); // 安全方法,直接放行
return;
}
// 4. 从请求中提取 Token(参数名 _csrf 或 Header X-CSRF-TOKEN)
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
// 5. 比对
if (!csrfToken.getToken().equals(actualToken)) {
throw new AccessDeniedException("Invalid CSRF Token"); // 403
}
chain.doFilter(request, response); // 放行
}
完整示例一:CookieCsrfTokenRepository 配合 SPA 前端
场景说明
前后端分离应用,前端使用 React 框架。Spring Security 后端提供 API,前端需要在 AJAX 请求中携带 CSRF Token。使用 CookieCsrfTokenRepository 将 Token 写入 XSRF-TOKEN Cookie,前端自动读取并发送 X-XSRF-TOKEN Header。
操作前配置(默认 Session 存储,SPA 无法获取)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
// 默认 HttpSessionCsrfTokenRepository
// Token 存在 Session 中,前端无法直接读取,POST 请求报 403
return http.build();
}
问题分析:默认 HttpSessionCsrfTokenRepository 将 Token 存入 HttpSession。SPA 前端无法直接读取 Session 中的 Token(除非通过后端接口获取),导致 AJAX 的 POST 请求因缺少 CSRF Token 而被 CsrfFilter 拒绝,返回 403。
操作后配置(Cookie 存储 + 允许前端读取)
@Configuration
@EnableWebSecurity
public class SpaCsrfConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf
// 使用 Cookie 存储 CSRF Token
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// withHttpOnlyFalse() 允许前端 JavaScript 读取 Cookie
);
return http.build();
}
}
前端 React 配合:
// 前端 Axios 配置:自动读取 XSRF-TOKEN Cookie 并发送 X-XSRF-TOKEN Header
import axios from 'axios';
// 设置 withCredentials 以携带 Cookie
axios.defaults.withCredentials = true;
// Axios 会自动读取名为 XSRF-TOKEN 的 Cookie 并发送 X-XSRF-TOKEN Header
// 这是 CookieCsrfTokenRepository 的默认约定
// 发起 POST 请求
axios.post('/api/transfer', {
to: 'user123',
amount: 100
}).then(response => {
console.log('转账成功', response.data);
}).catch(error => {
if (error.response && error.response.status === 403) {
console.error('CSRF Token 无效或缺失');
}
});
结果分析
| 请求 | 服务器行为 | 前端行为 | 结果 |
|---|---|---|---|
GET /api/account | CsrfFilter 生成 Token,存入 XSRF-TOKEN Cookie | 浏览器自动接收 Cookie | 页面加载,Cookie 中已有 Token |
POST /api/transfer | CsrfFilter 从 X-XSRF-TOKEN Header 读取 Token,与 Cookie 中的比对 | Axios 自动发送 X-XSRF-TOKEN Header | 200 成功 |
POST /api/transfer(无 Header) | CsrfFilter 未检测到 Token | 未发送 Header | 403 "Invalid CSRF Token" |
GET /api/account(再次) | CsrfFilter 重新生成 Token(如果原 Token 已过期) | 浏览器更新 Cookie | Cookie 中保持最新 Token |
完整示例二:为特定 API 路径禁用 CSRF 校验
场景说明
应用同时提供 Web 页面和 REST API。Web 页面需要 CSRF 防护(表单提交),但 API 端点被外部系统调用(无浏览器环境,无法携带 CSRF Token),需要为 /api/webhook/** 路径单独禁用 CSRF。
操作前配置(全局禁用,不安全)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()); // 危险!所有路径都失去 CSRF 防护
return http.build();
}
问题分析:csrf.disable() 全局关闭 CSRF 防护。Web 页面的表单登录、POST 操作等状态变更请求失去保护,攻击者可通过恶意页面伪造这些请求。
操作后配置(精确路径豁免)
@Configuration
@EnableWebSecurity
public class PartialCsrfConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/webhook/**").permitAll()
.requestMatchers("/web/**").authenticated()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf
// 仅对 /api/webhook/** 忽略 CSRF 校验
.ignoringRequestMatchers("/api/webhook/**")
// 其他路径(如 /web/** 的表单提交)仍受 CSRF 保护
);
return http.build();
}
}
测试验证与结果分析
| 请求路径 | 请求方法 | CSRF Token 要求 | 响应 |
|---|---|---|---|
/web/transfer | POST | 必须携带 _csrf | 无 Token → 403,有 Token → 200 |
/web/account | GET | 不需要(安全方法) | 200 |
/api/webhook/payment | POST | 不需要(已忽略) | 200(即使无 Token) |
/api/webhook/payment | GET | 不需要 | 200 |
/api/internal/update | POST | 必须携带(默认规则) | 无 Token → 403 |
易错场景:CsrfFilter 在过滤器链中的位置导致登录前校验失败
问题现象
开发者自定义了一个过滤器 CustomAuthFilter,试图在 CsrfFilter 之前解析某种自定义 Token 并完成认证。但发现 CustomAuthFilter 中的 POST 请求总是被 403 拒绝,原因是 CsrfFilter 在 CustomAuthFilter 之后执行——实际上,开发者真正遇到的问题是:在 Spring Security 5.x 的 HttpSecurity 配置中,CsrfFilter 的默认位置固定,但如果开发者试图用 addFilterBefore() 或 addFilterAfter() 添加自定义过滤器时,对 CsrfFilter 的位置理解错误。
错误配置
@Configuration
public class WrongFilterOrderConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
// 错误:在 CsrfFilter 之前添加自定义过滤器,但期望它跳过 CSRF 校验
.addFilterBefore(new MyApiTokenFilter(), CsrfFilter.class);
return http.build();
}
}
问题分析:MyApiTokenFilter 在 CsrfFilter 之前执行,如果 MyApiTokenFilter 中发起了对后续链的 POST 转发(如内部重定向),CsrfFilter 仍然会拦截。更常见的错误是:试图通过 addFilterBefore() 将某个自定义过滤器放在 CsrfFilter 前面以"绕过"CSRF 校验,但这是设计错误——CsrfFilter 的职责是在所有可能改变状态的操作前进行校验,不应被绕过。
正确做法
如果某条路径(如 /api/**)确实不需要 CSRF(如使用 JWT 的无状态 API),应通过 ignoringRequestMatchers() 或 disable() 在配置层面声明,而不是通过过滤器顺序 hack:
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.addFilterBefore(new JwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable()); // API 链明确禁用 CSRF
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/web/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.csrf(Customizer.withDefaults()); // Web 链保持 CSRF 开启
return http.build();
}
面试考点:
CsrfFilter为什么不对 GET 请求进行 Token 校验? 答:GET、HEAD、TRACE、OPTIONS 被定义为安全方法(Safe Methods),根据 HTTP 语义,它们不应改变服务器状态。CSRF 攻击的目标是诱导用户执行非自愿的状态变更操作(如转账、修改密码),因此CsrfFilter仅对 POST、PUT、DELETE、PATCH 等非安全方法进行 Token 校验。但需注意:如果应用中的 GET 请求实际会改变状态(如GET /delete?id=1),这是不安全的 API 设计,仅靠 CSRF 防护无法完全解决,应从 API 设计层面改为 POST/DELETE。