乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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)
CookieCsrfTokenRepositoryCookie(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/accountCsrfFilter 生成 Token,存入 XSRF-TOKEN Cookie浏览器自动接收 Cookie页面加载,Cookie 中已有 Token
POST /api/transferCsrfFilter 从 X-XSRF-TOKEN Header 读取 Token,与 Cookie 中的比对Axios 自动发送 X-XSRF-TOKEN Header200 成功
POST /api/transfer(无 Header)CsrfFilter 未检测到 Token未发送 Header403 "Invalid CSRF Token"
GET /api/account(再次)CsrfFilter 重新生成 Token(如果原 Token 已过期)浏览器更新 CookieCookie 中保持最新 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/transferPOST必须携带 _csrf无 Token → 403,有 Token → 200
/web/accountGET不需要(安全方法)200
/api/webhook/paymentPOST不需要(已忽略)200(即使无 Token)
/api/webhook/paymentGET不需要200
/api/internal/updatePOST必须携带(默认规则)无 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。

上一页
BasicAuthenticationFilter
下一页
CorsFilter