How do you configure CORS globally in Spring MVC, and when would you choose global config over @CrossOrigin?
answer
- WebMvcConfigurer.addCorsMappings(CorsRegistry)
- addMapping().allowedOrigins/Methods/Headers
- global default methods = GET/HEAD/POST only
- global + @CrossOrigin combine
- Security uses CorsConfigurationSource bean
basics
~10 sImplement WebMvcConfigurer and override addCorsMappings(CorsRegistry). Use registry.addMapping("/api/**") then chain allowedOrigins, allowedMethods, allowedHeaders, allowCredentials, maxAge. Prefer it over @CrossOrigin when you want one central, consistent policy across many controllers.
solid answer
~40 sRegister a @Configuration bean implementing WebMvcConfigurer and override addCorsMappings(CorsRegistry). Each registry.addMapping(pathPattern) returns a CorsRegistration you fluently configure: allowedOrigins/allowedOriginPatterns, allowedMethods, allowedHeaders, exposedHeaders, allowCredentials, maxAge. This centralizes the policy so it's auditable and applied uniformly by path pattern rather than scattered per controller. Global mappings and @CrossOrigin annotations both contribute — Spring combines them, with @CrossOrigin able to add to (not silently replace) the global rules for a specific handler. Choose global config for cross-cutting, app-wide policy — e.g. 'all /api/** endpoints accept the SPA origin' — and reserve @CrossOrigin for narrow, one-off exceptions. Note the global defaults are more restrictive than @CrossOrigin: addMapping defaults to all origins but only GET/HEAD/POST methods, so you usually must list methods explicitly.
code
java · 14 lines@Configuration
class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
}go deeper
Know the WebMvcConfigurer.addCorsMappings entry point and the fluent methods.
Know that global defaults restrict methods to GET/HEAD/POST and that policies combine.
Advocate centralizing policy; know the CorsConfigurationSource/CorsFilter alternative for Security.
Treat CORS as governed config: one source of truth, path-scoped, reviewed — avoid per-controller drift and over-permissive defaults.
## Two places CORS policy can live 1. **Annotation** — `@CrossOrigin` on controllers/methods (endpoint-scoped, decentralized). 2. **Global** — a `WebMvcConfigurer` overriding `addCorsMappings(CorsRegistry)` (path-scoped, centralized). They are **not** mutually exclusive; Spring's `CorsConfiguration.combine(...)` merges the global config for a matching path with any annotation on the matched handler. ## Global config skeleton ```java @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://app.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .exposedHeaders("X-Total-Count") .allowCredentials(true) .maxAge(3600); } } ``` ### The pieces - **`addMapping(pattern)`** — an Ant/`PathPattern` like `/api/**`. Returns a `CorsRegistration`. - **`allowedOrigins(...)`** — exact origins. `"*"` = any. - **`allowedOriginPatterns(...)`** — pattern-based origins (e.g. `https://*.example.com`); required if you also want credentials with a wildcard-ish match (see the credentials question). - **`allowedMethods(...)`** — HTTP verbs. **Default here is GET/HEAD/POST only**, so you almost always list them. `"*"` allows all. - **`allowedHeaders(...)`** — request headers the client may send. Default `"*"`. - **`exposedHeaders(...)`** — response headers JS may read beyond the safelist (e.g. a pagination count header). - **`allowCredentials(true)`** — permits cookies/Authorization to be sent and read. - **`maxAge(seconds)`** — how long the browser caches the preflight result. ## Global defaults differ from @CrossOrigin A subtle exam point: **`addMapping` defaults to all origins, all headers, but only GET/HEAD/POST methods**, maxAge 1800, credentials off. `@CrossOrigin`'s default methods are "whatever the mapping declares." So copying an annotation mindset into global config can silently drop PUT/DELETE. ## Alternative: a CorsFilter / CorsConfigurationSource bean Instead of `WebMvcConfigurer` you can register a `CorsConfigurationSource` (often a `UrlBasedCorsConfigurationSource`) and let a `CorsFilter` apply it. This is the form you typically use when **Spring Security** is present, because Security's own CORS support consumes a `CorsConfigurationSource` bean. ## When to choose which - **Global**: app-wide, consistent, auditable — the default choice for real apps ("the SPA origin can hit all `/api/**`"). - **@CrossOrigin**: a targeted exception for one endpoint that legitimately needs a different rule (e.g. a public webhook receiver). Keeping most policy global avoids the classic bug where a new controller silently ships with no (or an over-permissive) CORS rule.
- You added a global mapping for /api/** but PUT requests still fail preflight. Why?CorsRegistry's addMapping defaults to GET/HEAD/POST only. PUT isn't in the default set, so you must call allowedMethods and include PUT (or use "*").
- If Spring Security is on the classpath, is a WebMvcConfigurer enough?Often not. Security's filter chain runs before MVC; you should enable Security's CORS (http.cors()) and provide a CorsConfigurationSource bean so preflight OPTIONS is handled and not blocked by authorization.
saying these in an interview costs you the question
- Assuming global addMapping defaults allow all methods like @CrossOrigin does
- Thinking global config and @CrossOrigin conflict rather than combine
- Putting the SPA origin in allowedHeaders instead of allowedOrigins
- Relying on WebMvcConfigurer alone when Spring Security is present