CORS is the browser's same-origin gatekeeper: it decides whether JavaScript on origin A may read a response from origin B. It is not a Spring Security feature — it's a browser rule enforced via response headers (Access-Control-Allow-Origin, …) — but in a secured app you must let the CORS preflight OPTIONS request through before authentication, which is why where you configure it matters.
@CrossOrigin — per-controller
Annotate a controller or handler. Convenient for a single endpoint, but the policy is scattered across the codebase and easy to forget:
@CrossOrigin(origins = "https://app.example.com")
@GetMapping("/api/orders")
List<Order> orders() { ... }
WebMvcConfigurer.addCorsMappings — global, MVC level
Centralized CORS for the whole MVC layer:
@Configuration
class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry reg) {
reg.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
The catch: this runs at the DispatcherServlet, which is after the security filter chain. The preflight OPTIONS (which carries no credentials) can be rejected by Spring Security before it ever reaches MVC.
Spring Security CORS config — the right place for secured apps
Register a CorsConfigurationSource and enable CORS in the filter chain. Now CorsFilter runs early, handles preflight, and lets it pass authentication:
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults()); // picks up the bean below
return http.build();
}
@Bean
CorsConfigurationSource corsSource() {
CorsConfiguration c = new CorsConfiguration();
c.setAllowedOrigins(List.of("https://app.example.com"));
c.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
c.setAllowCredentials(true);
var src = new UrlBasedCorsConfigurationSource();
src.registerCorsConfiguration("/**", c);
return src;
}
Which to use
In any Spring Security app, configure CORS via http.cors(...) + a CorsConfigurationSource bean so the filter handles preflight correctly. Use @CrossOrigin only for quick, one-off endpoints in an unsecured app.