Developers configuring Cross-Origin Resource Sharing (CORS) in Spring Boot often find that standard controllers return the correct access headers, but endpoints managed by Spring Security—such as /login or /logout—omit them completely. When a browser client attempts a preflight OPTIONS request or cross-origin authentication call, it fails with the browser console error Access to XMLHttpRequest at 'http://example.com/login' from origin 'http://example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. This happens because Spring Security filters intercept and process requests before standard Spring MVC CORS configurations execute.
Quick fix
- Define a
CorsConfigurationSourcebean that explicitly specifies allowed origins, methods, and headers. - Inject that
CorsConfigurationSourceinto your Spring Security filter chain usinghttp.cors(cors -> cors.configurationSource(...)). - Ensure Spring Security permits unauthenticated preflight
OPTIONSrequests or allows the request to pass through the security filter chain. - Restart your application and clear your browser cache before testing.
Why this happens
- Filter Chain Execution Order: Spring Security processes incoming HTTP requests in its own filter chain (managed by
DelegatingFilterProxy) before the request ever reaches the Spring MVCDispatcherServletwhere `@CrossOrigin` or `WebMvcConfigurer` settings take effect. - Unhandled Preflight Requests: Browser engines send an HTTP
OPTIONSpreflight request prior to sending credentials or custom headers. If Spring Security rejects or intercepts theOPTIONSrequest without appending CORS headers, the browser drops the main request. - Missing CorsFilter Integration: Relying solely on MVC-level CORS configurations leaves security endpoints (like OAuth2, form login, or custom security filters) completely unprotected by CORS header injection rules.
How to fix it
Fixing CORS in Modern Spring Boot (Spring Boot 3+ / Spring Security 6+)
In modern Spring Security, Security configurations use a SecurityFilterChain bean instead of extending legacy base classes. You must wire a CorsConfigurationSource directly into the Security DSL so that Spring Security places the CorsFilter at the highest priority in its security chain.
Update your security configuration class (e.g., SecurityConfig.java) with the following structure:
package com.example.demo.config;
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;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.Customizer;
import java.util.List;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Enable CORS and integrate with the CorsConfigurationSource bean below
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()) // Configure CSRF according to your auth model
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Define origins allowed to make requests
configuration.setAllowedOrigins(List.of("https://example.com", "http://localhost:3000"));
// Specify allowed HTTP methods
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// Specify allowed request headers
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
// Allow credentials like cookies or HTTP authentication headers
configuration.setAllowCredentials(true);
// Expose headers if your frontend needs to read custom response headers
configuration.setExposedHeaders(List.of("Authorization", "Location"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// Apply this CORS configuration to all paths in the application
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Enforcing CorsFilter Priority via Filter Registration
If you have custom security filters executing before authentication, register a CorsFilter explicitly with high precedence so that CORS response headers are attached even if downstream security filters throw an authentication exception.
package com.example.demo.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Configuration
public class PriorityCorsConfig {
@Bean
public FilterRegistrationBean<CorsFilter> customCorsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("https://example.com"));
config.setAllowedHeaders(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
// Force the filter to execute before any security filters
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
Check that it worked
Verify that your security endpoints respond correctly to cross-origin preflight requests using curl from your web terminal or local console. Execute an HTTP OPTIONS request targeted specifically at your authentication URL:
curl -i -X OPTIONS https://example.com/login \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization, Content-Type"
Examine the output headers. A successful fix returns an HTTP/1.1 200 OK or 204 No Content status with the following response headers present:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Prevent it next time
- Do not rely strictly on
@CrossOriginannotations on@RestControllerclasses when using Spring Security; use a centralCorsConfigurationSourcebean instead. - Avoid setting
setAllowedOrigins(List.of("*"))alongsidesetAllowCredentials(true), as modern browser security standards reject wildcards when credential sharing is enabled. UsesetAllowedOriginPatternsif matching multiple subdomains dynamically. - Ensure that gateway proxies (such as Nginx or API Gateways) sitting in front of your Spring Boot app do not append duplicate
Access-Control-Allow-Originheaders.
FAQ
Why does @CrossOrigin work on regular controllers but fail on /login?
The @CrossOrigin annotation is handled by Spring MVC's HandlerMapping inside the DispatcherServlet. Endpoints generated by Spring Security filters (such as standard form login or OAuth endpoints) are intercepted before reaching the DispatcherServlet, rendering controller-level annotations ineffective.
Can I just allow all origins using "*"?
If your application sends credentials (such as cookies or HTTP Authorization headers), browsers explicitly reject the wildcard * in Access-Control-Allow-Origin response headers. You must specify exact origins or use origin patterns.
Should I handle CORS at the Nginx host layer or in Spring Boot?
Handling CORS inside Spring Boot ensures consistent application logic across development and production environments. However, if you run a reverse proxy like Nginx in front, ensure only one layer handles CORS to avoid duplicated origin headers.
Spring Apps on Dhruva Cloud
When running Java and Spring Boot applications, deploying behind an automated edge architecture simplifies network routing. Dhruva Cloud uses Cloudflare CDN and anycast DDoS protection in front of every environment, routing requests through secure encrypted tunnels straight to your application process.