Spring Boot CORS and CSRF

Browsers enforce strict safety rules, and two of them confuse many new developers. CORS controls which websites may call an API from a browser. CSRF protects logged-in users from forged requests sent by hostile pages. Both names look alike, yet they solve different problems.

Spring Boot and Spring Security handle each rule through short configuration blocks.

Understanding CORS

CORS stands for Cross-Origin Resource Sharing. An origin combines three parts: protocol, host, and port. Two addresses with any difference in those parts count as different origins.

Address AAddress BSame Origin?
http://localhost:8080http://localhost:8080/apiYes
http://localhost:3000http://localhost:8080No (port differs)
https://shop.comhttp://shop.comNo (protocol differs)
https://shop.comhttps://api.shop.comNo (host differs)

The Gated Community Idea

A gated community guard checks every visitor against a guest list. Residents walk in freely, while outsiders need an approved name on the list. The browser plays the guard. A page from one origin may call another origin only when the target server approves that origin in its reply headers.

 React app                      Spring Boot API
 localhost:3000                 localhost:8080
      |                               |
      |  GET /api/books  --------->   |
      |  Origin: localhost:3000       |
      |                               |
      | <---------  Access-Control-Allow-Origin: localhost:3000
      |                               |
   Browser allows the response    (guest list approved)

Without the approval header, the browser blocks the response and prints a CORS error in the console. The server itself ran the request; the browser simply refuses to hand the answer to the page.

Allowing One Controller

@RestController
@RequestMapping("/api/books")
@CrossOrigin(origins = "http://localhost:3000")
public class BookController {
    // endpoints
}

Allowing the Whole Application

A global configuration keeps the guest list in one place. When Spring Security is present, register the settings inside the security chain.

@Bean
public 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"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults());
    // other rules
    return http.build();
}

MaxAge tells the browser to cache the approval for one hour, which reduces repeated preflight checks.

Preflight Requests

Before a risky request, such as DELETE or one with an Authorization header, the browser sends a small OPTIONS request that asks permission. The server answers with the allowed methods and headers. Spring handles this exchange automatically once CORS is configured.

Understanding CSRF

CSRF stands for Cross-Site Request Forgery. Browsers attach cookies to every request sent to a site, even when a different page triggers the request. An attacker page can exploit that habit and send an action to a site where the victim already holds a login cookie.

The Rubber Stamp Idea

Imagine a company where any letter carrying the official stamp counts as approved. A stranger tricks an employee into stamping a blank letter, then fills in a money transfer. The stamp proves nothing about who wrote the content. A forged request rides on the login cookie in the same way.

 1. User logs in to bank.com          (cookie stored)
 2. User opens evil-page.com
 3. Evil page sends POST bank.com/transfer
 4. Browser attaches the bank cookie
 5. Bank sees a valid cookie  -->  action runs (without CSRF protection)

How the CSRF Token Defends

The server creates a secret random token for each session and expects that token inside every state-changing request. A hostile page cannot read the token because of the same-origin rule, so its forged request fails with a 403 error.

 Normal form:    token included  -->  request accepted
 Forged request: no token        -->  403 Forbidden

Spring Security turns CSRF protection on by default for session-based applications. Server-rendered forms receive the token automatically through Thymeleaf.

When to Disable CSRF

The risk exists because browsers attach cookies on their own. A stateless API that reads a JWT from the Authorization header carries no such cookie, so forged requests lack the header. Such APIs commonly disable CSRF.

http.csrf(csrf -> csrf.disable());

Keep CSRF protection active for any application that relies on session cookies.

Side-by-Side Summary

PointCORSCSRF
GoalControl which sites may read API responsesStop forged actions from hostile pages
Enforced byBrowser, using server headersServer, using a secret token
Typical fixAllow trusted originsRequire a CSRF token or use header-based tokens

Best Practices

  • List exact origins instead of the wildcard star, especially when cookies or credentials are involved.
  • Allow only the methods and headers that the client truly needs.
  • Test CORS from a real browser, because command-line tools ignore it.
  • Disable CSRF only for stateless, token-based APIs.
  • Use HTTPS in production so that cookies and tokens stay private.

Correct CORS and CSRF settings let trusted frontends work smoothly and block everything else. Understanding the difference saves hours of confusing browser errors.