skip to content

How does CORS interact with Spring Security's filter chain, and what is the correct way to configure CORS when Spring Security is present?

level: principalimportance: should knowfreq 55%

answer

  1. Security filters run before MVC CORS
  2. uncredentialed preflight OPTIONS => Security blocks it
  3. fix: http.cors(...) inserts CorsFilter early
  4. provide CorsConfigurationSource bean as single source
  5. permitAll(OPTIONS) doesn't add CORS headers; CSRF != CORS

basics

~20 s

Security filters run before Spring MVC, so an unauthenticated preflight OPTIONS can be blocked before MVC's CORS handling. Enable Security's own CORS with http.cors() and provide a CorsConfigurationSource bean; then Security applies CORS early and lets preflight through.

solid answer

~40 s

Spring Security's filter chain runs ahead of the DispatcherServlet. A CORS preflight is an unauthenticated OPTIONS with no cookies, so if Security demands authentication it rejects the preflight with 401/403 before MVC's CorsProcessor ever runs — the SPA sees a CORS failure even though your WebMvcConfigurer is correct. The fix is to make Security itself CORS-aware: call http.cors(Customizer.withDefaults()) (or http.cors(cors -> cors.configurationSource(...))). This adds Spring Security's CorsFilter early in the chain, which handles preflight and adds the allow headers before authorization runs. Security's CorsFilter looks up a CorsConfigurationSource bean — typically a UrlBasedCorsConfigurationSource — so define that bean as the single source of truth (it also feeds MVC). Do not try to permitAll() OPTIONS as a workaround; use the built-in CORS support, which correctly answers preflight and applies the policy to real requests too.

code

java · 26 lines
java
@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(Customizer.withDefaults())      // enable Security's CorsFilter (uses the bean below)
            .csrf(csrf -> csrf.disable())          // stateless token API
            .authorizeHttpRequests(a -> a.anyRequest().authenticated());
        return http.build();
    }

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

go deeper

for a junior

Awareness only: with Spring Security, CORS needs extra wiring beyond @CrossOrigin.

for a middle

Know to call http.cors() and provide a CorsConfigurationSource bean.

for a senior

Explain the filter-ordering reason preflight fails and why permitAll(OPTIONS) is insufficient.

for a principal

Own the design: single CorsConfigurationSource as source of truth, CORS as a reviewed trust boundary, correct filter placement, CSRF/CORS separation.

## The ordering problem Request processing order (roughly): **Servlet filters (incl. Spring Security's `FilterChainProxy`) → DispatcherServlet → HandlerMapping/CORS processing → controller.** Spring MVC's CORS support (`DefaultCorsProcessor`, driven by `@CrossOrigin`/`addCorsMappings`) lives **inside** the DispatcherServlet stage. Spring Security runs **before** that. A **CORS preflight** is an `OPTIONS` request that the browser sends **without credentials** (no cookies, no `Authorization`). So if your Security config requires authentication for the path, Security **rejects the preflight** (401/403) long before MVC's CORS processor could answer it. Result: the browser reports a CORS error, and developers waste hours because their `WebMvcConfigurer` *looks* perfect — it just never runs. ## The correct fix: Security's own CORS support Spring Security has first-class CORS handling. Enabling it inserts a **`CorsFilter`** early in the Security filter chain (before authorization filters), which: - Detects and **answers preflight OPTIONS** itself, and - Adds `Access-Control-Allow-*` headers to actual requests, - **before** the `authorizeHttpRequests` rules run. ### Enabling it (Spring Security 6 / lambda DSL) ```java @Bean SecurityFilterChain security(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) // <-- turns on Security's CorsFilter .csrf(csrf -> csrf.disable()) // for token/stateless APIs; keep CSRF for cookie sessions .authorizeHttpRequests(auth -> auth .anyRequest().authenticated()); return http.build(); } ``` `Customizer.withDefaults()` makes the `CorsFilter` consume a **`CorsConfigurationSource` bean** from the context. Or wire it explicitly: ```java http.cors(cors -> cors.configurationSource(corsConfigurationSource())); ``` ### The single source of truth Define one `CorsConfigurationSource` bean and let both Security and MVC use it: ```java @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration cfg = new CorsConfiguration(); cfg.setAllowedOriginPatterns(List.of("https://*.example.com")); cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); cfg.setAllowedHeaders(List.of("Authorization", "Content-Type")); cfg.setAllowCredentials(true); cfg.setMaxAge(3600L); UrlBasedCorsConfigurationSource src = new UrlBasedCorsConfigurationSource(); src.registerCorsConfiguration("/api/**", cfg); return src; } ``` With `http.cors(withDefaults())`, Spring Security auto-detects this bean. This avoids two divergent policies. ## Anti-patterns / gotchas - **`permitAll()` on OPTIONS** as a workaround (`.requestMatchers(HttpMethod.OPTIONS).permitAll()`) may unblock preflight, but it does **not** add the CORS response headers — the browser still fails. Use the CORS filter, not just permitAll. - **`@CrossOrigin` alone with Security** is unreliable: the preflight can be blocked before MVC. `@CrossOrigin` still works for the *actual* request headers, but preflight needs Security-level CORS. - **CSRF vs CORS**: they solve different problems. For **cookie-based sessions**, keep CSRF protection on; for **stateless token APIs**, CSRF is commonly disabled, but that's independent of CORS. Don't disable CSRF thinking it fixes CORS. - **Credentials trust boundary**: at principal level, treat credentialed CORS as an authorization decision. The allowed-origins list is part of your security posture and belongs in review; never widen it to `*`-like patterns for convenience (and it can't coexist with `allowCredentials` anyway). - **Filter placement**: Security's CORS support inserts the `CorsFilter` at the right point automatically; don't also register a stray `CorsFilter` bean that could double-process or conflict. ## Mental model - **Without Security**: MVC (`@CrossOrigin`/`addCorsMappings`) handles CORS. - **With Security**: make Security CORS-aware (`http.cors(...)` + a `CorsConfigurationSource` bean). Security answers preflight before auth; the same config governs actual requests. One bean, one policy.

  • Someone 'fixes' CORS by adding .requestMatchers(HttpMethod.OPTIONS).permitAll(). Why might the browser still fail?
    permitAll lets the OPTIONS through auth but nothing adds the Access-Control-Allow-* headers, so the browser still rejects the preflight. You need the CORS filter (http.cors) to actually write the CORS response, which also covers the real request.
  • Does http.cors() replace your WebMvcConfigurer addCorsMappings?
    It supersedes it for requests going through the Security chain by consuming a CorsConfigurationSource bean. Best practice is one CorsConfigurationSource bean as the single source of truth so Security and MVC don't diverge.
  • Is disabling CSRF required to make CORS work?
    No. CSRF and CORS are independent. CSRF is often disabled for stateless token APIs for other reasons, but that has nothing to do with CORS preflight/headers.

saying these in an interview costs you the question

  • Believing @CrossOrigin alone is sufficient once Spring Security is added
  • Thinking permitAll on OPTIONS adds CORS headers
  • Conflating CSRF and CORS ('disable CSRF to fix CORS')
  • Maintaining two divergent CORS policies (MVC config + Security) instead of one CorsConfigurationSource bean

context