intermediate~2h

CORS: Cross-Origin Resource Sharing

A frontend on one origin calling a backend on another is normal in modern architectures, and browsers block it by default. This chapter is what "origin" actually means and how to correctly unblock it.

Learning objectives

  • Beginner: State what makes two URLs the 'same origin' versus different origins.
  • Intermediate: Configure Spring Security's CORS support so a specific frontend origin can call the API.
  • Advanced: Explain why combining a wildcard origin with credentials is a dangerous anti-pattern, and what to do instead.

🌱 BEGINNER STORY

Imagine your Spring Boot API is an apartment building, and your Angular UI (running on a different port, like localhost:4200) is a delivery courier arriving from a different company. By default, the building's front desk (the browser's Same-Origin Policy) simply won't let ANY external courier past the lobby to deliver to a specific apartment — not because the apartment owner said no, but because the building's default policy is 'no outside couriers, ever, unless explicitly pre-approved.' CORS is the building management explicitly publishing a list: 'Couriers from this delivery company (this exact origin) ARE allowed up to apartments 3B and 4C (these specific endpoints), using these delivery methods (HTTP verbs), during these hours (with these headers).' Without that published list, the front desk turns every courier from a different origin away — even if the apartment owner (your API) would have been happy to receive the package.

An origin is the combination of scheme + host + port. http://localhost:4200 and http://localhost:8080 are DIFFERENT origins (different port) even though they're both 'localhost' — this trips up almost every beginner testing a full-stack app locally for the first time. The browser's Same-Origin Policy blocks JavaScript running on one origin from reading responses from a different origin UNLESS that different origin explicitly opts in via CORS headers.

Figure: For 'non-simple' requests, the browser automatically sends an OPTIONS preflight before the real request, checking whether the server allows it.

For anything beyond the simplest GET/POST requests (e.g., a request carrying a custom header like Authorization, or using PUT/DELETE, or a JSON content-type), the browser automatically fires an OPTIONS request FIRST — asking the server 'if I were to send this real request from this origin, with this method and these headers, would you allow it?' Only if the server responds with the right Access-Control-Allow-* headers does the browser proceed to send the actual request. Critically: this preflight is entirely a BROWSER behavior — your Spring Boot server must be configured to answer it correctly, or the browser will block the real request even though a tool like Postman (which doesn't enforce CORS) would work fine.

@Bean

SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) // ... rest of config ; return http.build(); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:4200")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); config.setAllowCredentials(true); // needed if the client sends cookies config.setMaxAge(3600L); // cache the preflight result for 1 hour UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }

Note: enabling CORS just for Spring MVC (@CrossOrigin annotation) is NOT enough if Spring Security is active — Spring Security's filter chain runs before your controller and must ALSO be told about the allowed origins via .cors(), otherwise the security layer itself may reject or mishandle the preflight.

⚠ Never do this together

setAllowedOrigins("*") combined with setAllowCredentials(true) is actually REJECTED by modern browsers as a security violation — allowing ANY origin to make credentialed (cookie-carrying) requests would let any malicious website silently make authenticated calls on behalf of a logged-in victim. If you truly need to allow multiple origins, list them explicitly (or use a pattern-matching allowedOriginPattern), never a bare wildcard, when credentials are involved.

⚠ Things that confuse teams

CORS is NOT a server-side security control against server-to-server calls — it is purely enforced by BROWSERS. A backend service, a mobile app, or a tool like curl/Postman completely ignores CORS headers; it exists only to protect browser users from malicious cross-origin JavaScript. A 'CORS error' in the browser console almost always means the SERVER's response is missing the right headers — it is not something you can fix purely from frontend code (beyond working around it with a proxy during development). Preflight (OPTIONS) requests must be allowed THROUGH the security filter chain without requiring authentication — otherwise the preflight itself gets a 401/403, which the browser interprets as 'not allowed,' blocking the real request before it's even sent. A wildcard subdomain setup (e.g., https://*.mycompany.com) needs setAllowedOriginPatterns() (not setAllowedOrigins(), which expects exact strings) to properly match multiple subdomains.

Want a visual for this concept?

Generate a diagram tailored to “CORS: Cross-Origin Resource Sharing” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to CSRF: Cross-Site Request Forgery← Back to all Spring Security chapters