What is the Spring Security filter chain, and why does the ORDER of filters in it matter?
answer
- FilterChainProxy = one servlet filter delegating to a list
- Each filter one job; later reads earlier's state
- Load context -> CSRF -> authenticate -> translate -> authorize
- SecurityContextHolder is ThreadLocal, bracketed early/late
- You never number filters yourself
basics
~20 sSpring Security is a chain of servlet filters run in a fixed order. Each filter does one job (load context, check CSRF, authenticate, authorize). Order matters because later filters depend on what earlier ones set up — e.g. you must load the user before checking permissions.
solid answer
~40 sSpring Security plugs into the servlet container as one filter (the FilterChainProxy) that delegates to an ordered list of internal filters — the SecurityFilterChain. Each filter has a single responsibility and runs in a deterministic order defined by FilterOrderRegistration. Order matters because filters build on each other: SecurityContextHolderFilter loads the SecurityContext (the current authentication) first; CsrfFilter validates the token; authentication filters like UsernamePasswordAuthenticationFilter populate the Authentication; ExceptionTranslationFilter wraps the tail to catch security exceptions; and AuthorizationFilter runs last to make the access decision once identity is known. Reorder them wrong and you'd authorize before authenticating, or lose exception handling. You never hand-number these filters — Spring places built-ins automatically and you insert custom ones relative to a known filter.
code
java · 18 lines@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Each of these DSL calls contributes a filter, placed by
// Spring in its predefined order (you don't order them).
.csrf(Customizer.withDefaults()) // CsrfFilter
.httpBasic(Customizer.withDefaults()) // BasicAuthenticationFilter
.formLogin(Customizer.withDefaults()) // UsernamePasswordAuthenticationFilter
.authorizeHttpRequests(auth -> auth // AuthorizationFilter (runs last)
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
}go deeper
Know it's an ordered chain of filters, each doing one job, and that order matters because later filters depend on earlier ones.
Name the key filters and their sequence, and that FilterChainProxy is the single registered servlet filter.
Explain the shared thread-local state (SecurityContextHolder) and why the ordering is load-bearing per filter.
Discuss FilterOrderRegistration as the source of truth and how DSL feature toggles add/remove filters deterministically.
## The big picture Spring Security does not intercept requests with magic — it registers a single standard **servlet `Filter`** with the container, named **`springSecurityFilterChain`** (an instance of **`FilterChainProxy`**). Every HTTP request passes through it. `FilterChainProxy` matches the request against one or more **`SecurityFilterChain`** objects and runs that chain's ordered list of internal Spring Security filters before (optionally) continuing to your controller. A **servlet filter** is a class implementing `jakarta.servlet.Filter` with a `doFilter(request, response, chain)` method. It can inspect/modify the request, then either call `chain.doFilter(...)` to continue, or short-circuit (e.g. send a 401). Spring Security is built as a *pipeline* of ~30 such filters, each with one narrow job. ## Why order is not arbitrary The filters share state through two thread-bound holders: - **`SecurityContextHolder`** — holds the `SecurityContext`, which holds the `Authentication` (who the caller is). - The request/response themselves. Because each filter reads what earlier filters wrote, the sequence is load-bearing. The canonical early-to-late flow: 1. **`SecurityContextHolderFilter`** — loads a previously saved `SecurityContext` (e.g. from the `HttpSession`) into `SecurityContextHolder` at the start of the request and clears it at the end. (In Spring Security 6 this replaced the older `SecurityContextPersistenceFilter`; it no longer *creates* a context eagerly.) Must run early so everything downstream sees the current user. 2. **`CsrfFilter`** — for state-changing requests, validates the CSRF token before any authentication logic acts on the request body. 3. **`UsernamePasswordAuthenticationFilter`** (and siblings like `BasicAuthenticationFilter`, `BearerTokenAuthenticationFilter`) — attempt authentication and, on success, store the `Authentication` in the context. 4. **`ExceptionTranslationFilter`** — wraps everything *after* it in a try/catch. It converts a thrown `AuthenticationException` into a 401 / redirect-to-login and an `AccessDeniedException` into a 403 (or login for anonymous users). 5. **`AuthorizationFilter`** (Spring Security 6; replaced `FilterSecurityInterceptor`) — the authorization decision point. It runs *last*, once identity is established, and throws `AccessDeniedException` if the caller lacks the required authority. Note the crucial pairing: **`ExceptionTranslationFilter` sits immediately before `AuthorizationFilter`** so it can catch the `AccessDeniedException` that `AuthorizationFilter` throws. A filter only catches exceptions from filters *after* it. ## Where the order comes from You do not assign numbers. Spring Security's **`FilterOrderRegistration`** hard-codes the relative order of every built-in filter, and the DSL (`HttpSecurity`) adds them in that order based on which features you enable. You influence order only by inserting *custom* filters relative to a known one via `addFilterBefore`, `addFilterAfter`, or `addFilterAt`. ## Gotchas - Enabling/disabling a feature (e.g. `.csrf().disable()`) removes that filter entirely — it doesn't just no-op. - Filters that short-circuit (send a response without calling `chain.doFilter`) stop the pipeline; nothing downstream runs. - The `SecurityContextHolder` is `ThreadLocal` by default, so the load-early/clear-late bracketing by `SecurityContextHolderFilter` prevents context leaking across pooled threads.
- Is Spring Security one filter or many, from the servlet container's point of view?One: the container sees a single `springSecurityFilterChain` filter (`FilterChainProxy`). Internally that proxy delegates to an ordered list of Spring Security filters — the `SecurityFilterChain`.
- How do filters pass 'who the user is' down the chain?Through `SecurityContextHolder`, a thread-local holding the `SecurityContext` and its `Authentication`. Authentication filters write it; `AuthorizationFilter` and your code read it.
saying these in an interview costs you the question
- Thinking each SecurityFilterChain filter is registered separately with the servlet container (only FilterChainProxy is).
- Believing you assign numeric order to Spring's built-in filters.
- Saying authorization happens before authentication.