Solving Spring Boot CORS Errors Once and For All
Writing
JAVA ENGINEERING
December 22, 20254 min read

Solving Spring Boot CORS Errors Once and For All

The definitive guide to fixing Access-Control-Allow-Origin errors in Spring Boot. Learn the difference between @CrossOrigin, Global Config, and Security Filters.

javaspring-bootcorssecurityweb-development

If you are a full-stack developer, this error haunts your dreams:

Access to fetch at http://localhost:8080/api from origin http://localhost:3000 has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

You just want to connect your React frontend to your Spring Boot backend. Why is the browser stopping you?

CORS Nightmare Debug

What is CORS? (It's not a bug)

CORS (Cross-Origin Resource Sharing) is a security feature protecting the browser, not the server.

Without CORS, a malicious site (evil.com) could make a request to bank.com content while you are logged in. The browser blocks cross-origin requests by default unless the server explicitly says "It's okay, I trust evil.com".

The Preflight Check

Before sending a specialized request (like a POST with JSON), the browser sends a "Preflight" OPTIONS request.

CORS Handshake Flow

If your server doesn't reply to OPTIONS with 200 OK and the correct Access-Control-Allow-Origin headers, the browser blocks the real request.

Solution 1: Global Configuration (The Best Way)

If you want to apply rules to your entire application, implement WebMvcConfigurer. This is the cleanest approach.

@Configuration
public class CorsConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // Apply to all endpoints
            .allowedOrigins("http://localhost:3000", "https://mydomain.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600); // Cache the preflight response for 1 hour
    }
}

Why this is good: It keeps your configuration central. You define your frontend URL in one place (perfect for pulling from application.properties).

Solution 2: @CrossOrigin (The Quick Way)

If you only need to open up a single specific endpoint, you can annotate the controller.

@RestController
@RequestMapping("/api/public")
// Allow ALL origins (Not recommended for prod)
@CrossOrigin(origins = "*") 
public class PublicController {
 
    @GetMapping("/hello")
    public String hello() {
        return "Hello World";
    }
}

Why this is bad: It clutters your business code. Code duplication. If you have 50 controllers, do you paste this 50 times?

Solution 3: Spring Security (The "Gotcha")

If you are using Spring Security, the solutions above might NOT WORK.

Why? Because Spring Security sits before Spring MVC in the filter chain. It will intercept the OPTIONS request and reject it (401 Unauthorized) before it ever reaches your WebMvcConfigurer settings.

You must configure CORS inside the SecurityFilterChain.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 1. Enable CORS in Security
            .cors(Customizer.withDefaults())
            
            // 2. Disable CSRF (Usually needed for stateless APIs)
            .csrf(csrf -> csrf.disable())
            
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
 
        return http.build();
    }
 
    // 3. Define the source
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Summary Checklist

  1. Just Spring Boot? Use WebMvcConfigurer (Solution 1).
  2. Using Spring Security? Use cors(Customizer.withDefaults()) and a CorsConfigurationSource Bean (Solution 3).
  3. Authentication Cookies? You MUST set .allowCredentials(true) AND you cannot use * for origins. You must specify the exact domain.

CORS errors are frustrating, but they are just the browser trying to keep your users safe. Configure it once, centrally, and sleep soundly.

For deeper understanding, see the MDN Web Docs on CORS, the Spring Framework CORS Support Documentation, and the Fetch Living Standard on CORS Protocol.

Keep Reading

Happy Coding! ☕

Frequently Asked Questions

What causes CORS errors in Spring Boot?

CORS errors occur when a browser blocks a request from one origin (e.g., localhost:3000) to a different origin (e.g., localhost:8080) because the server did not include the Access-Control-Allow-Origin header in its response. This is a browser security feature, not a server bug.

What is the difference between @CrossOrigin and global CORS configuration?

@CrossOrigin is applied per-controller or per-method and is good for quick fixes. Global CORS configuration via WebMvcConfigurer applies to all endpoints and is the recommended approach for production applications because it centralizes CORS rules in one place.

Why does CORS not work when Spring Security is enabled?

Spring Security processes requests before Spring MVC, so it can block CORS preflight (OPTIONS) requests before they reach your CORS configuration. You need to enable CORS in your SecurityFilterChain using http.cors() and provide a CorsConfigurationSource bean.

Should I set Access-Control-Allow-Origin to * in production?

No. Using a wildcard (*) allows any website to make requests to your API, which is a security risk. In production, specify the exact origins that should be allowed, such as your frontend domain.

Last updated: April 2, 2026

Rabinarayan Patra

Rabinarayan Patra

SDE II at Amazon. Previously at ThoughtClan Technologies building systems that processed 700M+ daily transactions. I write about Java, Spring Boot, microservices, and the things I figure out along the way. More about me →

X (Twitter)LinkedIn

Stay in the loop

Get the latest articles on system design, frontend & backend development, and emerging tech trends — straight to your inbox. No spam.