Hook: CORS is like a building receptionist: your backend may be open, but the browser still asks, Who is allowed to walk in from another website?
Question: How do you configure CORS in Spring Boot, especially when Spring Security is on the classpath?
Answer: CORS means Cross-Origin Resource Sharing, a browser rule that controls which websites may call your API. In Spring Boot, you can configure it at three levels: a single controller with @CrossOrigin, global MVC rules with WebMvcConfigurer, or, in a secured app, a CorsConfigurationSource plus http.cors(). The key idea is that the browser enforces CORS, so your server must return the right headers for the browser to let JavaScript read the response.
Interview-Ready Answer: I configure CORS by allowing only the origins, methods, and headers I trust, usually in a global CORS bean or at the security filter chain. In Spring Security, I make sure to enable http.cors() and provide a CorsConfigurationSource, because otherwise preflight requests can fail before they reach my controller. I also avoid using a wildcard origin with credentials, and I set a sensible maxAge so browsers can cache preflight results for a while, often around 3600 seconds.
Detailed Explanation: A cross-origin request is one where the scheme, host, or port is different. For example, https://app.example.com calling https://api.example.com is cross-origin because the host differs. The browser applies the same-origin policy first, then checks whether the API explicitly says, This origin is allowed.
Origin header.OPTIONS request first. A preflight is a permission check before the real request.Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers.maxAge period and then sends the real request.@CrossOrigin on one controller or method when you want a quick local exception.WebMvcConfigurer#addCorsMappings when your app is mostly MVC and you want one central policy.CorsConfigurationSource bean and enable http.cors(). This is the most important option when authentication and filters are involved.| Approach | Best for | Pros | Caveat |
|---|---|---|---|
| @CrossOrigin | One endpoint | Fast and local | Easy to forget elsewhere |
| WebMvcConfigurer | Whole MVC app | Central policy | May be bypassed by Security |
| CorsConfigurationSource | Secured APIs | Works with filters | Must wire Security correctly |
*.GET, POST, and often OPTIONS for preflight.Authorization and Content-Type if the browser will send them.true only when you really need cookies or HTTP auth. Do not combine it with a wildcard origin; the browser spec forbids that pattern.X-Total-Count.3600 is common and saves extra OPTIONS calls.The runtime cost is tiny: CORS matching is effectively O(1) per request because Spring checks a small configuration object and writes headers. The bigger cost is network chatter from preflight requests, which is why maxAge matters. A common gotcha is that your API may look fine in Postman or curl, but the browser still blocks it; that is because browsers enforce CORS, not HTTP clients.
http.cors(), the security filter chain can reject the preflight before the controller is reached.Authorization header, the browser often preflights, so that header must be allowed.allowedOriginPatterns can be useful, but use it carefully and prefer explicit origins when possible.Origin header, CORS decisions may become confusing, so always test from the real browser path.Memory-friendly rule: allow the fewest origins, methods, and headers that your app truly needs, then let the browser cache the permission check for a sensible time.
Real-World Story: Imagine a checkout service with a React frontend on https://shop.example.com and a Spring Boot API on https://api.example.com. The frontend sends authenticated requests with an Authorization header, so the browser performs a preflight before each new route or header combination. The team configures the API in curl, everything looks fine, and then users suddenly see the checkout page spin forever in the browser.
http.cors() call in Spring Security.Access-Control-Allow-Origin header is present, even though the server returned 200 or 401.OPTIONS request followed by a security rejection, or no controller log at all because the preflight never passed.In production, this is especially painful because the backend team thinks the API is healthy, but the browser is silently refusing to hand the response to JavaScript. The fix is to make the CORS policy explicit, test it from the real frontend origin, and keep the security filter chain and CORS config in sync.
package com.example.corsdemo;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@SpringBootApplication
public class CorsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CorsDemoApplication.class, args);
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Spring Security must explicitly delegate CORS handling, or preflight OPTIONS requests
// can fail before your controller is ever reached.
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// Browsers send preflight requests without credentials.
// If OPTIONS is blocked here, the real request never happens.
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/public").permitAll()
.requestMatchers("/api/secure").authenticated()
.anyRequest().permitAll()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://shop.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
config.setExposedHeaders(List.of("X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
@Bean
UserDetailsService userDetailsService() {
// In a real app, use a proper password encoder and a real user store.
return new InMemoryUserDetailsManager(
User.withUsername("demo")
.password("{noop}password")
.roles("USER")
.build()
);
}
@RestController
@RequestMapping("/api")
static class ApiController {
@GetMapping("/public")
public Map<String, String> publicEndpoint() {
return Map.of("message", "Public data is available to the allowed browser origins.");
}
@GetMapping("/secure")
public Map<String, String> secureEndpoint(Principal principal) {
return Map.of(
"message", "Secure data is available after authentication.",
"user", principal.getName()
);
}
@GetMapping("/cors-note")
public Map<String, String> corsNote() {
// Edge case note: if a browser calls this endpoint from a disallowed origin,
// the server can still return 200, but JavaScript cannot read the response.
// That is why CORS failures often look like frontend bugs, not backend bugs.
return Map.of("note", "CORS is enforced by the browser, not by curl.");
}
}
}
Follow-up & Tricky Questions:
Authorization or Content-Type: application/json in some cases. The server must allow that preflight for the real request to continue.CorsConfigurationSource and http.cors(). MVC-only settings can be fine in non-secured apps, but security can intercept requests first.* and still send cookies? No. If credentials are allowed, you must name trusted origins explicitly, or use a safe origin pattern strategy that echoes a real origin.Content-Type and often Authorization. If your frontend reads custom response headers, add them to exposedHeaders.@CrossOrigin everywhere? It is fine for a small exception, but large apps usually need one central policy so you do not create inconsistent rules across endpoints.Tricky / Gotcha Questions:
OPTIONS in allowedMethods? Usually yes, because the browser may preflight with OPTIONS. If you forget it, the real request may never start.Common Mistakes:
* with credentials: fix it by listing explicit origins, because cookies and wildcard origins do not mix.http.cors() so Spring Security lets the preflight through.OPTIONS: fix it by allowing preflight requests, especially when using Authorization or JSON requests from the browser.Memory Hook: Think: Origin, Method, Header, Cache. If those four do not line up, the browser stays outside the door.
Cheat Sheet:
@CrossOrigin for one-off endpoints.WebMvcConfigurer for app-wide MVC settings.CorsConfigurationSource plus http.cors() in Spring Security apps.allowCredentials carefully and cache preflight with maxAge.Practice Tasks:
OPTIONS from the allowed methods and observe the preflight failure.allowCredentials to false and see how cookie-based calls change.