CORS Configuration
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls whether a web page from one origin may call an API on a different origin. When your React/Angular front end on http://localhost:3000 calls a Spring Boot API on http://localhost:8080, the browser enforces CORS. This page shows per-handler, controller, and global configuration, plus the Spring Security interaction.
What CORS actually is
The same-origin policy blocks JavaScript from reading responses from a different origin (scheme + host + port) unless the server opts in via CORS headers. For non-simple requests the browser first sends a preflight OPTIONS request; the server must answer with the right Access-Control-Allow-* headers before the real request is sent.
Browser (localhost:3000) Server (localhost:8080)
--- OPTIONS /api/users ------------> (preflight)
<-- 200 + Access-Control-Allow-* ---
--- GET /api/users ---------------->
<-- 200 + data ---------------------
Note: CORS is enforced by the browser, not the server. Tools like
curland server-to-server calls are unaffected. CORS is not a substitute for authentication or authorization.
@CrossOrigin — per controller or method
The quickest way to allow a specific origin is @CrossOrigin on a controller or handler.
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
@GetMapping
public List<UserResponse> all() { ... }
@PostMapping
@CrossOrigin(origins = "https://admin.example.com") // method-level override
public UserResponse create(@RequestBody CreateUserRequest body) { ... }
}
Common attributes:
@CrossOrigin(
origins = {"https://app.example.com"},
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "*",
allowCredentials = "true",
maxAge = 3600)
Global configuration with WebMvcConfigurer
For consistent rules across all controllers, register CORS mappings in a WebMvcConfigurer.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Global configuration with CorsConfigurationSource
When you also use Spring Security, the recommended approach is a CorsConfigurationSource bean, which both MVC and Security can pick up.
import org.springframework.context.annotation.*;
import org.springframework.web.cors.*;
import java.util.List;
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
Interaction with Spring Security
Security’s filter chain runs before the dispatcher servlet, so a misconfigured chain will block preflight OPTIONS requests before MVC’s CORS handling sees them. Enable CORS inside the SecurityFilterChain so Security applies your CorsConfigurationSource.
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> {}) // uses the CorsConfigurationSource bean above
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
}
Warning: You cannot combine
allowCredentials(true)withallowedOrigins("*")— the browser rejects it. UseallowedOriginPatterns("*")if you genuinely need wildcard origins with credentials.
| Approach | Scope | Best for |
|---|---|---|
@CrossOrigin | Single controller/method | Quick, localized rules |
WebMvcConfigurer | All MVC handlers | App-wide rules, no Security |
CorsConfigurationSource | MVC + Security | Apps using Spring Security |
Verifying a preflight
Request:
curl -i -X OPTIONS http://localhost:8080/api/users \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST"
Output:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Pitfalls
- Forgetting
.cors(...)in theSecurityFilterChainmakes preflights fail even when MVC CORS is configured. allowedHeaders("*")does not echo credentials-related headers automatically; list custom auth headers explicitly when needed.- CORS errors appear in the browser console, not the server log — check the network tab.