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 | 是否允许携带 Cookie | true |
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 | 任意 | 否 | GET | 200,允许跨域 |
/api/public/weather | 任意 | 是(试图携带) | GET | 200,但浏览器不会发送 Cookie(allowCredentials=false) |
/api/internal/users | https://admin.example.com | 是 | OPTIONS + GET | 200,正常携带 Cookie |
/api/internal/users | https://evil.com | 是 | OPTIONS | 预检失败,无 CORS 响应头,浏览器拦截 |
/web/dashboard | https://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,避免双重配置导致的不一致和调试困难。