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

    • 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
    • 最佳实践

CorsFilter

定义与作用

CorsFilter 是 Spring Security 过滤器链中处理 跨域资源共享(CORS) 的过滤器。它位于 HeaderWriterFilter 之后、CsrfFilter 之前,负责拦截浏览器的**预检请求(Preflight,OPTIONS 方法)**和实际跨域请求,通过设置 Access-Control-Allow-* 响应头告知浏览器是否允许跨域访问。如果请求不符合配置的 CORS 策略,浏览器会在客户端层面拦截,不会发送到实际业务逻辑。


核心原理

1. CorsConfigurationSource:CORS 配置源

CorsFilter 本身不直接决定 CORS 策略,而是委托给 CorsConfigurationSource:

public interface CorsConfigurationSource {
    // 根据请求路径返回对应的 CORS 配置
    CorsConfiguration getCorsConfiguration(HttpServletRequest request);
}

CorsConfiguration 包含:

配置项说明典型值
allowedOrigins允许的来源域"https://example.com"
allowedMethods允许的 HTTP 方法GET, POST, PUT, DELETE
allowedHeaders允许的请求头Authorization, Content-Type
allowCredentials是否允许携带 Cookietrue
maxAge预检结果缓存时间(秒)3600
exposedHeaders允许客户端读取的响应头X-Request-Id

2. 预检请求 vs 实际请求的处理差异

3. 与 Spring MVC @CrossOrigin 的关系

Spring Security 的 CorsFilter 与 Spring MVC 的 @CrossOrigin 注解是两个独立机制:

  • @CrossOrigin:由 Spring MVC 的 HandlerMethod 拦截器处理,在 Controller 方法层面生效。
  • CorsFilter:在 Spring Security 过滤器链中生效,优先于 @CrossOrigin。如果 CorsFilter 拒绝(不添加响应头),请求不会到达 Controller。

当 Spring Security 启用 CORS 时,通常建议统一在 CorsFilter 中配置,避免两者冲突。


完整示例一:精确配置 CORS(允许特定域名 + 凭证)

场景说明

前端应用部署在 https://app.example.com,后端 API 在 https://api.example.com。需要允许跨域请求,且前端需要携带 Cookie 和 Authorization Header。必须同时配置 allowedOrigins、allowCredentials 和 allowedHeaders。

操作前配置(全局允许或配置不完整)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .cors(Customizer.withDefaults());  // 使用 Spring 默认 CORS 配置
    return http.build();
}

// Spring Boot 默认会尝试查找 CorsConfigurationSource Bean
// 如果没有自定义,可能默认允许所有来源,这在生产环境不安全

问题分析:Customizer.withDefaults() 如果没有对应的 CorsConfigurationSource Bean,可能不会按预期工作。更危险的是,某些开发者会写成:

// 错误做法:允许所有来源且允许凭证
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true);  // 与 * 冲突!浏览器会拒绝

浏览器规范规定:Access-Control-Allow-Origin: * 与 Access-Control-Allow-Credentials: true 不能同时出现,否则浏览器会拒绝响应。

操作后配置(精确配置)

@Configuration
@EnableWebSecurity
public class PreciseCorsConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 1. 明确指定允许的域名(不能用 * 配合 allowCredentials)
        config.setAllowedOrigins(List.of(
            "https://app.example.com",
            "https://admin.example.com"
        ));
        
        // 2. 允许的 HTTP 方法
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        
        // 3. 允许的请求头
        config.setAllowedHeaders(List.of(
            "Authorization", 
            "Content-Type", 
            "X-Requested-With"
        ));
        
        // 4. 允许客户端读取的响应头
        config.setExposedHeaders(List.of("X-Request-Id", "X-Total-Count"));
        
        // 5. 允许携带 Cookie(必须与明确的 allowedOrigins 配合)
        config.setAllowCredentials(true);
        
        // 6. 预检请求缓存 1 小时,减少 OPTIONS 请求次数
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        // 为不同路径配置不同策略
        source.registerCorsConfiguration("/web/**", new CorsConfiguration().applyPermitDefaultValues());
        
        return source;
    }
}

结果分析

请求场景浏览器行为服务器响应结果
https://evil.com 访问 /api/data发送请求(带 Origin)比对 allowedOrigins,https://evil.com 不在列表中,不添加 CORS 响应头浏览器拒绝响应,控制台报 CORS 错误
https://app.example.com GET /api/data发送请求添加 Access-Control-Allow-Origin: https://app.example.com正常响应,浏览器允许读取
https://app.example.com POST /api/data 带 Authorization先发送 OPTIONS 预检预检通过,返回 Access-Control-Allow-Methods: POST, PUT... 和 Access-Control-Allow-Headers: Authorization预检通过后发送实际 POST,正常响应
携带 Cookie 的请求发送请求Access-Control-Allow-Credentials: true 已设置浏览器允许携带和接收 Cookie

完整示例二:不同路径使用不同的 CORS 策略

场景说明

应用同时提供:

  • 公共 API(/api/public/**):允许所有来源访问,但不允许凭证(公开数据,无需登录)。
  • 内部 API(/api/internal/**):仅允许 https://admin.example.com 访问,允许凭证(需要管理员登录)。
  • Web 页面(/web/**):不允许跨域(同源访问)。

操作前配置(单策略,无法满足差异化需求)

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("*"));
    config.setAllowedMethods(List.of("*"));
    config.setAllowCredentials(true);  // 与 * 冲突,浏览器拒绝!
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);  // 所有路径同一策略
    return source;
}

问题分析:单策略 /** 无法区分公开 API 和内部 API 的不同安全需求。且 * + allowCredentials(true) 在浏览器层面会报错,导致所有跨域请求失败。

操作后配置(路径级差异化策略)

@Configuration
@EnableWebSecurity
public class PathBasedCorsConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/internal/**").hasRole("ADMIN")
                .requestMatchers("/web/**").authenticated()
                .anyRequest().denyAll()
            )
            .formLogin(Customizer.withDefaults())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // 1. 公开 API:允许所有来源,不允许凭证
        CorsConfiguration publicConfig = new CorsConfiguration();
        publicConfig.setAllowedOrigins(List.of("*"));
        publicConfig.setAllowedMethods(List.of("GET", "POST"));
        publicConfig.setAllowedHeaders(List.of("Content-Type"));
        publicConfig.setAllowCredentials(false);  // 明确关闭凭证
        publicConfig.setMaxAge(3600L);
        source.registerCorsConfiguration("/api/public/**", publicConfig);
        
        // 2. 内部 API:仅允许管理后台,允许凭证
        CorsConfiguration internalConfig = new CorsConfiguration();
        internalConfig.setAllowedOrigins(List.of("https://admin.example.com"));
        internalConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        internalConfig.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        internalConfig.setExposedHeaders(List.of("X-Total-Count"));
        internalConfig.setAllowCredentials(true);  // 需要登录 Cookie
        internalConfig.setMaxAge(3600L);
        source.registerCorsConfiguration("/api/internal/**", internalConfig);
        
        // 3. Web 页面:不配置 CORS(默认不允许跨域)
        // 不注册 /web/** 路径,CorsFilter 不添加任何响应头
        
        return source;
    }
}

测试验证与结果分析

请求路径来源域是否带 Cookie预检/实际结果
/api/public/weather任意否GET200,允许跨域
/api/public/weather任意是(试图携带)GET200,但浏览器不会发送 Cookie(allowCredentials=false)
/api/internal/usershttps://admin.example.com是OPTIONS + GET200,正常携带 Cookie
/api/internal/usershttps://evil.com是OPTIONS预检失败,无 CORS 响应头,浏览器拦截
/web/dashboardhttps://evil.com否GET无 CORS 响应头,浏览器默认同源策略拦截

易错场景:CorsFilter 与 Spring MVC @CrossOrigin 配置冲突

问题现象

开发者在 Spring Security 中配置了 CorsFilter,同时又在 Controller 上加了 @CrossOrigin:

// Spring Security 配置:仅允许 https://app.example.com
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowCredentials(true);
    // ...
}

// Controller 配置:允许所有来源
@RestController
@CrossOrigin(origins = "*")  // 与 Security 配置冲突!
public class DataController {
    @GetMapping("/api/data")
    public ResponseEntity<?> getData() { ... }
}

问题分析:CorsFilter 在 Spring Security 过滤器链中执行,优先于 Spring MVC 的 @CrossOrigin 拦截器。请求到达 CorsFilter 时,如果 CorsConfigurationSource 配置为仅允许 https://app.example.com,则来自 https://other.com 的请求会在 CorsFilter 阶段被拒绝(不添加 CORS 响应头),请求根本不会到达 @CrossOrigin 生效的阶段。但如果 CorsFilter 允许通过(如 CorsConfigurationSource 配置较宽松),而 @CrossOrigin 配置更严格,两者的差异会导致行为不一致——某些跨域请求通过 CorsFilter 但可能被 @CrossOrigin 拒绝,反之亦然。

更隐蔽的问题是:如果 CorsFilter 已经处理过请求(添加了响应头),Spring MVC 的 CorsInterceptor 会检测到响应中已有 CORS 头,跳过自身处理。这意味着 @CrossOrigin 实际上被 CorsFilter 的响应覆盖或忽略,开发者误以为 @CrossOrigin 生效,实则由 CorsFilter 主导。

正确做法

在 Spring Security 项目中,统一在 CorsFilter 层配置 CORS,避免与 @CrossOrigin 混用:

@Configuration
@EnableWebSecurity
public class UnifiedCorsConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://app.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

// Controller 中不再使用 @CrossOrigin
@RestController
public class DataController {
    @GetMapping("/api/data")
    public ResponseEntity<?> getData() {
        // 专注业务逻辑,CORS 由 CorsFilter 统一处理
        return ResponseEntity.ok(dataService.load());
    }
}

面试考点:CorsFilter 与 Spring MVC @CrossOrigin 同时存在时,哪个生效?如何避免冲突? 答:CorsFilter 在 Spring Security 过滤器链中执行,位于 Spring MVC DispatcherServlet 之前,因此它先于 @CrossOrigin 生效。如果 CorsFilter 已经添加了 CORS 响应头,Spring MVC 的 CorsInterceptor 会跳过处理。因此两者同时存在时,CorsFilter 的配置实际上主导了 CORS 行为。最佳实践是统一在 Spring Security 层配置 CORS,彻底移除 Controller 上的 @CrossOrigin,避免双重配置导致的不一致和调试困难。

上一页
CsrfFilter
下一页
HeaderWriterFilter