In a Spring Security app, why configure CORS via the CorsConfigurationSource in the filter chain rather than WebMvcConfigurer.addCorsMappings or @CrossOrigin?
answer
- filter chain runs before DispatcherServlet
- MVC CORS = too late, authz already rejected
- http.cors() + CorsConfigurationSource bean
- implicit MVC-source fallback is fragile
- don't configure CORS at both layers (dup headers)
basics
~20 sThe security filter chain runs before Spring MVC. If CORS lives only in WebMvcConfigurer or @CrossOrigin, the security filters can reject a preflight before MVC handles it. Configuring a CorsConfigurationSource plus http.cors() puts CORS inside the filter chain where it runs first.
solid answer
~40 sSpring MVC's CORS options — WebMvcConfigurer.addCorsMappings and @CrossOrigin — are handled inside the DispatcherServlet, which sits after the entire Spring Security filter chain. A cross-origin preflight (credential-less OPTIONS) or even the real request can be rejected by the authorization filter before it ever reaches MVC, so MVC-level CORS never runs. The fix is to register a CorsConfigurationSource bean and call http.cors(Customizer.withDefaults()); Spring Security then installs a CorsFilter early in the chain that applies your CorsConfiguration before authorization. A subtle detail: when http.cors() is enabled and no explicit CorsConfigurationSource is provided, Spring Security can fall back to the MVC CorsConfigurationSource, but the reliable, explicit approach in a secured app is a dedicated bean wired through the security DSL so ordering is guaranteed.
code
java · 25 lines// Preferred in a secured app: CORS owned by the security chain.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // CorsFilter runs before authorization
.authorizeHttpRequests(a -> a.anyRequest().authenticated());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// Avoid relying on this alone when Security guards the routes:
// @Configuration
// class WebConfig implements WebMvcConfigurer {
// public void addCorsMappings(CorsRegistry r) { r.addMapping("/**")... }
// }go deeper
May not distinguish the security filter layer from the MVC layer.
Knows http.cors() is needed but may not articulate the ordering-vs-MVC reason.
Must explain the two-layer ordering, the fallback nuance, and duplicate-header risk.
Sets a codebase-wide convention for where CORS lives and reasons about deterministic wiring across profiles/gateways.
## Two layers, two places CORS can live A Spring Boot web request passes through: 1. The **servlet filter chain**, including Spring Security's chain (authentication, authorization, etc.). 2. Then the **DispatcherServlet** (Spring MVC), which dispatches to your `@Controller` methods. CORS can be configured at either layer: - **MVC layer:** `@CrossOrigin` on controllers/methods, or `WebMvcConfigurer.addCorsMappings(CorsRegistry)`. Handled by MVC's `AbstractHandlerMapping` / `DefaultCorsProcessor` inside the DispatcherServlet. - **Security/filter layer:** `http.cors()` + a `CorsConfigurationSource` bean, applied by a `CorsFilter` in the security chain. ## Why the layer matters Because the filter chain runs **first**, if you lock requests down (`anyRequest().authenticated()`) and put CORS only at the MVC layer, the sequence for a preflight is: 1. Browser sends credential-less `OPTIONS`. 2. Security authorization filter sees an unauthenticated request → 401/403. 3. The request never reaches the DispatcherServlet, so your MVC CORS config is irrelevant. 4. Browser aborts the real request → generic CORS error. Even for non-preflighted requests, security may reject before MVC responds, and the response then lacks CORS headers, so the browser blocks it. ## The correct pattern in a secured app ```java http.cors(Customizer.withDefaults()); ``` plus a `CorsConfigurationSource` bean. This puts the `CorsFilter` inside the security chain, ahead of authorization, guaranteeing that CORS headers are written and preflights are answered regardless of the authorization rules. ## The fallback nuance If you call `http.cors(Customizer.withDefaults())` but define **no** `CorsConfigurationSource` bean, Spring Security's `CorsConfigurer` will try to reuse an MVC-provided source (it can pick up a bean named `corsConfigurationSource`, or in some setups the MVC `CorsConfigurationSource`). Relying on that implicit wiring is fragile; the recommended approach in a security-first app is an explicit `CorsConfigurationSource` bean so behavior is deterministic and independent of MVC. ## When @CrossOrigin is still fine If the application has **no** Spring Security on the classpath (or the relevant paths are fully permitted and there is no authorization that could reject a preflight), `@CrossOrigin` / `addCorsMappings` at the MVC layer works. The problem is specifically the interaction with a locked-down security filter chain. Even then, mixing both layers can produce duplicated or conflicting `Access-Control-Allow-*` headers, so pick one place — in a secured app, that place is the security chain. ## Gotcha: duplicate headers Configuring CORS at both layers can emit the allow headers twice or with different values, which some browsers treat as invalid. Consolidate on the `CorsConfigurationSource` + `http.cors()` approach. ## When to use Any Spring Boot app that has Spring Security and serves a cross-origin browser client should own CORS in the security layer. Reserve MVC-layer CORS for apps without a security filter chain guarding those routes.
- What symptom tells you CORS is configured at the wrong layer?Cross-origin requests fail and the preflight OPTIONS returns 401/403 instead of 200 with Access-Control-Allow-* headers, because the security authorization filter rejected it before MVC's CORS config could run.
- Can you configure CORS at both the security layer and the MVC layer?You can, but it risks duplicated or conflicting Access-Control-Allow-* headers, which some browsers reject. In a secured app, consolidate on http.cors() with a CorsConfigurationSource bean.
saying these in an interview costs you the question
- Insisting @CrossOrigin always works even behind a locked-down security chain
- Not knowing the filter chain runs before the DispatcherServlet
- Configuring CORS at both layers without realizing it can duplicate headers