RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#528 min readJul 11, 2026

How do you configure CORS?

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What CORS actually does

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.

  1. The browser sends a request with an Origin header.
  2. If the request is simple, it may go straight through. If it uses non-simple methods or headers, the browser sends a preflight OPTIONS request first. A preflight is a permission check before the real request.
  3. Spring Security and Spring MVC evaluate the incoming request against the configured CORS rules.
  4. If the origin, method, and headers are allowed, Spring returns headers such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers.
  5. The browser stores the result for the maxAge period and then sends the real request.
  6. If the headers do not match, the server may still respond, but the browser blocks JavaScript from reading the response.

Ways to configure it in Spring Boot

  1. Controller level: use @CrossOrigin on one controller or method when you want a quick local exception.
  2. Global MVC level: implement WebMvcConfigurer#addCorsMappings when your app is mostly MVC and you want one central policy.
  3. Security level: in Spring Security, define a CorsConfigurationSource bean and enable http.cors(). This is the most important option when authentication and filters are involved.
ApproachBest forProsCaveat
@CrossOriginOne endpointFast and localEasy to forget elsewhere
WebMvcConfigurerWhole MVC appCentral policyMay be bypassed by Security
CorsConfigurationSourceSecured APIsWorks with filtersMust wire Security correctly

Important settings and why they matter

  • allowedOrigins: list the exact sites you trust. This is safer than *.
  • allowedMethods: include the verbs your frontend uses, such as GET, POST, and often OPTIONS for preflight.
  • allowedHeaders: add headers like Authorization and Content-Type if the browser will send them.
  • allowCredentials: set this to true only when you really need cookies or HTTP auth. Do not combine it with a wildcard origin; the browser spec forbids that pattern.
  • exposedHeaders: use this when your frontend must read response headers such as X-Total-Count.
  • maxAge: this is the preflight cache time in seconds. A value like 3600 is common and saves extra OPTIONS calls.

Performance and edge cases

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.

  • If you use Spring Security and forget http.cors(), the security filter chain can reject the preflight before the controller is reached.
  • If you send the Authorization header, the browser often preflights, so that header must be allowed.
  • If you need many subdomains, allowedOriginPatterns can be useful, but use it carefully and prefer explicit origins when possible.
  • If a reverse proxy strips the 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.

  1. The root cause is usually a missing CORS rule or a missing http.cors() call in Spring Security.
  2. The visible symptom is a browser console error like, No Access-Control-Allow-Origin header is present, even though the server returned 200 or 401.
  3. In logs, the server may show an OPTIONS request followed by a security rejection, or no controller log at all because the preflight never passed.
  4. Users experience broken login, empty carts, or failed payment submission because the frontend cannot read the API response.

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.

Spring Boot
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:

  • How does CORS differ from CSRF? CORS controls which cross-origin browsers may read responses; CSRF is about stopping a browser from sending unwanted authenticated actions. They solve different problems, and turning on CORS does not disable CSRF protection.
  • Why is preflight happening? The browser sends an OPTIONS request when the method or headers are not simple, such as when you use Authorization or Content-Type: application/json in some cases. The server must allow that preflight for the real request to continue.
  • Where should I configure CORS in a Spring Security app? Prefer the security chain with a CorsConfigurationSource and http.cors(). MVC-only settings can be fine in non-secured apps, but security can intercept requests first.
  • Can I use * 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.
  • What headers do I need for a SPA? Usually Content-Type and often Authorization. If your frontend reads custom response headers, add them to exposedHeaders.
  • Does curl prove CORS works? No. Curl ignores browser security rules, so always test with a browser or a real frontend app.
  • Can I rely on @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.
  • Why does a GET fail if CORS is about POST? GET can still fail if the origin is not allowed or if the browser needs to send credentials and the server does not permit them.
  • Should I allow all headers in development? You can, but tighten them before production. Broad dev settings often hide bugs that appear later when the frontend adds auth or file upload headers.

Tricky / Gotcha Questions:

  • If my controller returns 200, is CORS successful? Not necessarily. The browser only treats it as successful if the response includes the correct CORS headers for that origin, method, and credential mode.
  • Do I need OPTIONS in allowedMethods? Usually yes, because the browser may preflight with OPTIONS. If you forget it, the real request may never start.
  • Is CORS a server-side security feature? It is mostly a browser policy. The server participates by sending headers, but the browser is the one that enforces the rule.

Common Mistakes:

  • Using * with credentials: fix it by listing explicit origins, because cookies and wildcard origins do not mix.
  • Configuring only MVC in a secured app: fix it by also enabling http.cors() so Spring Security lets the preflight through.
  • Forgetting OPTIONS: fix it by allowing preflight requests, especially when using Authorization or JSON requests from the browser.
  • Testing only with Postman or curl: fix it by testing from the real frontend origin, because those tools do not enforce browser CORS rules.

Memory Hook: Think: Origin, Method, Header, Cache. If those four do not line up, the browser stays outside the door.

Cheat Sheet:

  • CORS is a browser rule for cross-origin reads.
  • Use @CrossOrigin for one-off endpoints.
  • Use WebMvcConfigurer for app-wide MVC settings.
  • Use CorsConfigurationSource plus http.cors() in Spring Security apps.
  • Allow only trusted origins, methods, and headers.
  • Set allowCredentials carefully and cache preflight with maxAge.

Practice Tasks:

  • Add a second allowed frontend origin and verify the response headers.
  • Remove OPTIONS from the allowed methods and observe the preflight failure.
  • Switch allowCredentials to false and see how cookie-based calls change.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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."); } } }