Contents
- How CSRF Works
- Disabling CSRF for Stateless REST APIs
- Cookie-Based CSRF Token (SPAs)
- How CORS Works
- Global CORS Configuration
- Per-Endpoint @CrossOrigin
- Combining CSRF & CORS in SecurityFilterChain
Spring Security's CSRF protection works by issuing a server-side token that must be echoed back in every state-changing request (POST, PUT, DELETE). A malicious site cannot read the token from your session cookie due to the browser's Same-Origin Policy, so it cannot forge a valid request.
CSRF protection is enabled by default in Spring Security. For traditional server-rendered web apps with sessions it should stay on. For stateless REST APIs authenticated via JWT (no session, no cookies) it can safely be disabled.
If your API is stateless (JWT in Authorization header, no session cookie), CSRF attacks are impossible — disable CSRF to avoid 403 Forbidden on mutating requests.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // safe — no session cookies
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
Never disable CSRF in an application that uses session cookies for authentication. Only disable it when authentication relies on a stateless mechanism like a Bearer token in the Authorization header.
For SPAs (Angular, React) that use session cookies, use CookieCsrfTokenRepository. Spring writes the CSRF token into a JavaScript-readable cookie (XSRF-TOKEN); the SPA reads it and echoes it as an X-XSRF-TOKEN header on each mutating request.
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // JS-readable
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())) // Spring 6 handler
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated());
return http.build();
}
// Angular HttpClient automatically sends the X-XSRF-TOKEN header.
// For plain fetch in React / vanilla JS:
function getCookie(name) {
return document.cookie.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1];
}
fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCookie('XSRF-TOKEN')
},
body: JSON.stringify(order)
});
Browsers block cross-origin AJAX requests by default. When your React app on http://localhost:3000 calls your API on http://localhost:8080, the browser first sends a preflight OPTIONS request to check if the server permits it. Spring Security must handle CORS before authentication checks — otherwise the preflight (which carries no credentials) is rejected with 401.
Register a CorsConfigurationSource bean and enable it inside SecurityFilterChain. This applies CORS rules to every endpoint, and Spring Security processes the preflight before any auth check.
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 java.util.List;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://app.example.com",
"http://localhost:3000" // dev frontend
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-XSRF-TOKEN"));
config.setExposedHeaders(List.of("X-Total-Count")); // headers JS can read
config.setAllowCredentials(true); // required if frontend sends cookies
config.setMaxAge(3600L); // preflight cache TTL in seconds
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
For fine-grained control on individual controllers or methods, use @CrossOrigin. It complements (not replaces) global config.
import org.springframework.web.bind.annotation.*;
// Applied at class level — all methods in this controller
@CrossOrigin(origins = "https://partner.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping
public List<Product> list() { ... }
// Override origin for one method
@CrossOrigin(origins = "*") // public endpoint — no credentials needed
@GetMapping("/catalog")
public List<Product> publicCatalog() { ... }
}
Do not use origins = "*" with allowCredentials = true — browsers reject that combination. Use explicit origin lists whenever cookies or Authorization headers are involved.
A typical production SecurityFilterChain for a stateless REST API with a separate SPA frontend:
@Bean
public SecurityFilterChain apiSecurityChain(HttpSecurity http,
CorsConfigurationSource corsSource) throws Exception {
http
// CORS — must be first, before auth
.cors(cors -> cors.configurationSource(corsSource))
// CSRF — disabled for stateless JWT auth
.csrf(csrf -> csrf.disable())
// No session — JWT is stateless
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Auth rules
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // always allow preflight
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
// JWT filter before UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}