Spring Boot: CORS Configuration
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which domains can access your API. By default, browsers block requests from a different origin than the server. Spring Boot provides multiple ways to configure CORS: from annotation-level control on individual controllers to global configuration with WebMvcConfigurer and integration with Spring Security.
Contents
What is CORS
Browsers enforce the Same-Origin Policyhttps://app.example.comhttps://api.example.com
CORS (Cross-Origin Resource Sharing) is the HTTP-header-based protocol that relaxes this policy. The browser and server exchange a set of Access-Control-*
Key CORS Response Headers
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origins are allowed (e.g. https://app.example.com* |
Access-Control-Allow-Methods | Permitted HTTP methods (GET, POST, PUT, DELETE, etc.) |
Access-Control-Allow-Headers | Permitted request headers (e.g. AuthorizationContent-Type |
Access-Control-Allow-Credentials | Whether cookies / authorization headers are allowed (truefalse |
Access-Control-Max-Age | How long (seconds) the browser may cache the preflight response |
Access-Control-Expose-Headers | Which response headers the browser can expose to JS |
Simple vs. Preflight Requests
A simple request uses GET, HEAD, or POST with standard headers and content types: the browser sends it directly and checks the response headers. Any other combination triggers a preflight: the browser sends an OPTIONS request first, and only proceeds with the actual request if the server responds with the correct CORS headers.
PUTDELETEAuthorizationContent-Type: application/json@CrossOrigin on Controllers
The simplest way to enable CORS in Spring Boot is the @CrossOrigin
Class-Level @CrossOrigin
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "https://app.example.com")
public class ProductController {
@GetMapping
public List<Product> listProducts() {
return productService.findAll();
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productService.save(product);
}
}
Every endpoint in ProductControllerhttps://app.example.com
Method-Level @CrossOrigin
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@CrossOrigin(origins = {"https://app.example.com", "https://admin.example.com"},
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = {"Authorization", "Content-Type"},
maxAge = 3600)
@GetMapping
public List<Order> listOrders() {
return orderService.findAll();
}
// This endpoint has NO CORS — browser blocks cross-origin access
@DeleteMapping("/{id}")
public void deleteOrder(@PathVariable Long id) {
orderService.delete(id);
}
}
@CrossOrigin Attribute Reference
| Attribute | Default | Description |
|---|---|---|
origins | * | Allowed origins (use originPatterns*.example.com |
methods | The mapped method(s) | Allowed HTTP methods |
allowedHeaders | * | Allowed request headers |
exposedHeaders | empty | Response headers exposed to JavaScript |
allowCredentials | false | Whether to allow cookies and auth headers |
maxAge | 1800 | Preflight cache duration in seconds |
allowCredentialstrueorigins = "*"originPatternsGlobal CORS with WebMvcConfigurer
For applications with many controllers, configuring CORS globally via WebMvcConfigurer@CrossOrigin
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("Authorization", "Content-Type", "X-Request-ID")
.exposedHeaders("X-Total-Count", "X-Request-ID")
.allowCredentials(true)
.maxAge(3600);
// Separate config for public endpoints
registry.addMapping("/public/**")
.allowedOrigins("*")
.allowedMethods("GET")
.maxAge(86400);
}
}
The addMapping()addMapping()
WebMvcConfigurer@CrossOriginCORS with Spring Security
When Spring Security is on the classpath, you must configure CORS through the security filter chain. Spring Security's CorsFilter
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://app.example.com",
"https://admin.example.com"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-ID"));
config.setExposedHeaders(List.of("X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
cors(Customizer.withDefaults())CorsConfigurationSourcecorsConfigurationSourceUsing Environment-Specific Origins
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsSourceConfig {
@Value("${cors.allowed-origins}")
private List<String> allowedOrigins;
@Value("${cors.allowed-methods:GET,POST,PUT,DELETE}")
private List<String> allowedMethods;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(allowedOrigins);
config.setAllowedMethods(allowedMethods);
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
Then in application.yml
# application-dev.yml
cors:
allowed-origins: http://localhost:3000,http://localhost:5173
allowed-methods: GET,POST,PUT,DELETE,PATCH
---
# application-prod.yml
cors:
allowed-origins: https://app.example.com,https://admin.example.com
allowed-methods: GET,POST,PUT,DELETE
CORS Configuration Properties
Whether you use @CrossOriginWebMvcConfigurerCorsConfigurationSource
| Property | Type | Description |
|---|---|---|
allowedOrigins |
List<String> |
Exact origin URLs. Use "*"https://app.example.com |
allowedOriginPatterns |
List<String> |
Pattern-based origins with wildcard support. Example: https://*.example.comallowCredentials(true) |
allowedMethods |
List<String> |
HTTP methods to allow. Default is the mapped handler methods. Use "*" |
allowedHeaders |
List<String> |
Request headers the client may send. Default is "*"Access-Control-Allow-Headers |
exposedHeaders |
List<String> |
Response headers the browser should expose to client-side JavaScript. Defaults to empty (only CORS-safelisted headers exposed). |
allowCredentials |
Boolean |
Whether to include cookies and AuthorizationtrueallowedOrigins"*" |
maxAge |
Long |
Duration in seconds for which the preflight response is cached by the browser. Default: 1800 (30 minutes). Max varies by browser (Chrome caps at 2 hours). |
allowedOriginPatterns Example
CorsConfiguration config = new CorsConfiguration();
// Allow any subdomain of example.com over HTTPS
config.setAllowedOriginPatterns(List.of("https://*.example.com"));
// Allow localhost on any port (useful during development)
config.setAllowedOriginPatterns(List.of("http://localhost:*"));
// Combine patterns
config.setAllowedOriginPatterns(List.of(
"https://*.example.com",
"http://localhost:*"
));
config.setAllowCredentials(true); // works with patterns, unlike origins="*"
Preflight Requests
A preflight request is an HTTP OPTIONS
Anatomy of a Preflight Exchange
# 1. Browser sends OPTIONS preflight
OPTIONS /api/products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
# 2. Server responds with CORS headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
Content-Length: 0
# 3. Browser sends actual request (only if preflight passed)
POST /api/products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{"name": "Widget", "price": 29.99}
Caching Preflight Responses
The Access-Control-Max-Age
- Chrome caps max-age at 7200 seconds (2 hours)
- Firefox caps at 86400 seconds (24 hours)
- Set a reasonable value like
(1 hour) for production3600 - Use
during development to disable caching and always see preflight requests in DevTools0
Debugging Preflight Issues
Open Chrome DevTools, go to the Network tab, and filter by method:OPTIONS
@RequestMapping(method = RequestMethod.OPTIONS)Common CORS Errors
CORS errors show up in the browser console, not in server logs. Here are the most common issues and how to fix them.
1. "No 'Access-Control-Allow-Origin' header is present"
Cause: The server did not return the Access-Control-Allow-Origin
- Verify the origin in the browser request matches exactly what is configured (scheme + host + port)
- Check that the path pattern in
oraddMapping() covers the requested URLregisterCorsConfiguration() - If using Spring Security, ensure
is enabled in.cors()SecurityFilterChain
2. "Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'"
Cause: You have set allowCredentials(true)allowedOrigins("*")
- Replace
with specific originsallowedOrigins("*") - Or use
which supports credentialsallowedOriginPatterns("*")
3. "Method PUT is not allowed by Access-Control-Allow-Methods"
Cause: The HTTP method used is not included in the server's allowedMethods
- Add the missing method:
.allowedMethods("GET", "POST", "PUT", "DELETE")
4. "Request header field Authorization is not allowed"
Cause: The AuthorizationallowedHeaders
- Add it explicitly:
.allowedHeaders("Authorization", "Content-Type") - Or use
to allow all headers.allowedHeaders("*")
5. Preflight Returns 401/403
Cause: Spring Security is intercepting the OPTIONS request and requiring authentication before CORS headers are sent.
- Ensure
is configured in the security filter chain: it places the.cors() before authentication filtersCorsFilter - Avoid manually permitting OPTIONS in
: let the CORS filter handle itauthorizeHttpRequests
6. CORS Works Locally but Fails in Production
- Check if a reverse proxy (Nginx, CloudFront) is stripping or overwriting CORS headers
- Verify the production origin URL matches exactly (trailing slashes, www vs non-www)
- Ensure the proxy forwards the
header to the backendOrigin
Production Best Practices
Never Use Wildcard Origins in Production
Using allowedOrigins("*")allowedOriginPatterns
// BAD — allows any website to call your API
config.setAllowedOrigins(List.of("*"));
// GOOD — only your frontend domains
config.setAllowedOrigins(List.of(
"https://app.example.com",
"https://admin.example.com"
));
// GOOD — all subdomains of your domain
config.setAllowedOriginPatterns(List.of("https://*.example.com"));
Use Environment-Specific Configuration
Externalize CORS origins via Spring profiles so development and production have different allowed origins.
@Configuration
public class CorsConfig {
@Bean
@Profile("dev")
public CorsConfigurationSource devCorsSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
@Profile("prod")
public CorsConfigurationSource prodCorsSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setExposedHeaders(List.of("X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
Restrict Allowed Methods and Headers
Only allow the HTTP methods and headers your API actually uses. Avoid allowedMethods("*")allowedHeaders("*")
Set a Reasonable maxAge
A maxAge
Coordinate with Reverse Proxies
If Nginx or a CDN sits in front of your Spring Boot app, make sure CORS headers are set in only one place. Having both the proxy and the app set CORS headers can cause duplicate headers, which browsers reject.
# Nginx — pass through CORS headers from Spring Boot, do NOT add your own
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header Origin $http_origin;
# Do NOT add add_header Access-Control-* here if Spring handles CORS
}
Security Checklist
- Never use
withallowedOrigins("*") : it is rejected by browsers and exposes cookies to all sitesallowCredentials(true) - Keep
minimal: only expose headers your frontend actually readsexposedHeaders - Use HTTPS for all allowed origins in production
- Do not rely on CORS as your only security mechanism: it is a browser-enforced policy and does not protect against server-to-server requests
- Review CORS configuration as part of your security audit process
- Test CORS with
to verify unauthorized origins are blockedcurl -H "Origin: https://evil.com" -v