Spring Security: CSRF Protection & CORS Configuration
CSRF (Cross-Site Request Forgery) and CORS (Cross-Origin Resource Sharing) are two separate browser security concerns that Spring Security handles differently. CSRF protects stateful sessions from forged requests; CORS controls which origins browsers allow to call your API. Both need deliberate configuration for REST APIs and SPAs.
Contents
How CSRF Works
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.
Disabling CSRF for Stateless REST APIs
If your API is stateless (JWT in Authorization403 Forbidden
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();
}
}
AuthorizationCookie-Based CSRF Token (SPAs)
For SPAs (Angular, React) that use session cookies, use CookieCsrfTokenRepositoryXSRF-TOKENX-XSRF-TOKEN
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)
});
How CORS Works
Browsers block cross-origin AJAX requests by default. When your React app on http://localhost:3000http://localhost:8080OPTIONS401
Global CORS Configuration
Register a CorsConfigurationSourceSecurityFilterChain
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;
}
}
Per-Endpoint @CrossOrigin
For fine-grained control on individual controllers or methods, use @CrossOrigin
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() { ... }
}
origins = "*"allowCredentials = trueCombining CSRF & CORS in SecurityFilterChain
A typical production SecurityFilterChain
@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();
}