1
0

Add the cors-csrf module

This commit is contained in:
2026-08-28 09:33:22 +05:30
parent 73ab67b171
commit cad813e1ae
49 changed files with 3338 additions and 13 deletions

View File

@@ -0,0 +1,82 @@
# 1. Two layers, one word
*Next: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md)*
A Spring Boot application can be told about CORS in two entirely separate places, and the two
places do not talk to each other unless you make them.
**The MVC layer.** `WebMvcConfigurer.addCorsMappings(..)` and `@CrossOrigin` register a
`CorsConfiguration` with Spring MVC's handler mappings. It is consulted inside
`DispatcherServlet`, when the request is being matched to a handler method.
**The security layer.** `HttpSecurity.cors(..)` puts a `org.springframework.web.filter.CorsFilter`
into the security filter chain. It runs at order **1000** — between `HeaderWriterFilter`
(900) and `CsrfFilter` (1100), and a long way above `AuthorizationFilter` (4200).
The security filter chain runs to completion before `DispatcherServlet` is ever entered. So if
the security chain rejects a request, MVC's CORS configuration is not merely ignored: the code
that reads it never executes.
## Why that specifically breaks preflights
A CORS preflight is not a special protocol. It is an ordinary `OPTIONS` request carrying two
headers:
```
OPTIONS /api/data HTTP/1.1
Origin: https://spa.example.com
Access-Control-Request-Method: POST
```
The browser sends it **without credentials** — no cookies, no `Authorization` header, by
design. Against `anyRequest().authenticated()` that request is anonymous, and anonymous requests
are denied. The rejection happens at order 4200, three thousand two hundred slots before
`DispatcherServlet` and about four thousand before your `addCorsMappings` call matters.
[`docs/output/01-mvc-only.txt`](output/01-mvc-only.txt) is that state: an eleven-filter chain
with no `CorsFilter` in it, and a preflight answered `401`.
## The fix is one line, and it is not on the MVC layer
```java
http.cors(Customizer.withDefaults())
```
That is [`MvcBridgeSecurityConfig`](../src/main/java/com/ankurm/cors/config/MvcBridgeSecurityConfig.java),
and [`docs/output/02-mvc-bridge.txt`](output/02-mvc-bridge.txt) is the identical application
answering `200`. The MVC configuration was fine all along. Nothing was reading it.
`CorsFilter` short-circuits every preflight it sees:
```java
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return; // the chain stops here
}
filterChain.doFilter(request, response);
```
Read that `if` carefully, because chapter 2 turns on it: the filter returns on **every**
preflight, whether or not it found a configuration to apply.
## One thing that changes when you move the configuration
Moving CORS from `addCorsMappings` to a `CorsConfigurationSource` bean is not a pure
relocation. Compare the two transcripts:
```
02-mvc-bridge.txt Access-Control-Max-Age: 1800
03-security-source.txt (nothing)
```
`CorsRegistration` — the builder behind `addCorsMappings` — defaults `maxAge` to
1800 seconds. A bare `CorsConfiguration` leaves it `null`, and a preflight response with no
`Access-Control-Max-Age` is not cached, so the browser preflights **every single cross-origin
call**. Two round trips instead of one, forever, with nothing in any log to suggest it.
```java
configuration.setMaxAge(1800L);
```
---
*Next: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md)*