Walk through the roles and relative order of SecurityContextHolderFilter, CsrfFilter, UsernamePasswordAuthenticationFilter, ExceptionTranslationFilter, and AuthorizationFilter.
answer
- Holder loads context, clears in finally (ThreadLocal safety)
- CSRF only on mutating verbs, throws AccessDeniedException
- UsernamePassword = POST /login, AbstractAuthenticationProcessingFilter
- ExceptionTranslation catches only DOWNSTREAM: 401 vs 403
- AuthorizationFilter last, replaced FilterSecurityInterceptor
basics
~10 sOrder: SecurityContextHolderFilter loads the current user; CsrfFilter checks the CSRF token; UsernamePasswordAuthenticationFilter authenticates form logins; ExceptionTranslationFilter catches security errors from below it; AuthorizationFilter (last) decides if the user may access the resource.
solid answer
~40 sEarly to late: **SecurityContextHolderFilter** loads any saved `SecurityContext` into `SecurityContextHolder` and clears it after the request. **CsrfFilter** validates the CSRF token on state-changing requests before request-body-consuming auth runs. **UsernamePasswordAuthenticationFilter** handles form-login POSTs, delegating to the `AuthenticationManager` and storing the resulting `Authentication`. **ExceptionTranslationFilter** wraps the rest of the chain in try/catch, turning `AuthenticationException` into a 401/login redirect and `AccessDeniedException` into a 403. **AuthorizationFilter** runs last as the decision point, consulting an `AuthorizationManager` and throwing `AccessDeniedException` when authority is insufficient. The key relationship: `ExceptionTranslationFilter` sits directly before `AuthorizationFilter` so it can translate the exceptions that authorization throws — a filter only catches errors from filters downstream of it.
code
java · 16 lines// Conceptual sketch of ExceptionTranslationFilter's position/behavior:
// It wraps everything AFTER it, so it can catch what AuthorizationFilter throws.
public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
try {
chain.doFilter(req, res); // -> ... -> AuthorizationFilter (may throw)
} catch (AuthenticationException ex) {
// Not authenticated: kick off login
sendStartAuthentication(req, res, ex); // AuthenticationEntryPoint -> 401 / redirect
} catch (AccessDeniedException ex) {
if (isAnonymous()) {
sendStartAuthentication(req, res, ex); // anonymous -> login
} else {
accessDeniedHandler.handle(req, res, ex); // authenticated -> 403
}
}
}go deeper
Just get the sequence and one-line role of each of the five filters.
Explain the ExceptionTranslationFilter/AuthorizationFilter adjacency and the 401-vs-403 branching.
Add the SS5->SS6 renames (SecurityContextPersistenceFilter->Holder, FilterSecurityInterceptor->AuthorizationFilter) and why lazy context loading matters.
Reason about custom-filter placement relative to ExceptionTranslationFilter and the consequences for error handling and session creation.
## The five filters, in order ### 1. `SecurityContextHolderFilter` (early) At the *start* of each request it uses a `SecurityContextRepository` (default: `HttpSessionSecurityContextRepository`) to **load** a previously persisted `SecurityContext` into the thread-local `SecurityContextHolder`; at the *end* (in a `finally`) it **clears** the holder so context never leaks across pooled request threads. In Spring Security 6 it replaced the older **`SecurityContextPersistenceFilter`**; the difference is that it only *loads* the context (lazily, via a `Supplier`) and does **not** eagerly create/save an empty one — saving is now the responsibility of the authentication mechanism. It must run early because every downstream filter reads `SecurityContextHolder`. ### 2. `CsrfFilter` (before request-consuming auth) For **mutating** requests (anything not in the safe set: GET, HEAD, OPTIONS, TRACE) it compares the token supplied by the client (header or parameter) against the expected token from the `CsrfTokenRepository`. Mismatch or missing token -> it throws `AccessDeniedException` (specifically `InvalidCsrfTokenException` / `MissingCsrfTokenException`) and the request is rejected. It runs before form/login processing so a forged POST is stopped before credentials logic touches the body. ### 3. `UsernamePasswordAuthenticationFilter` (middle) A subclass of `AbstractAuthenticationProcessingFilter`. By default it matches `POST /login`, extracts username/password, builds a `UsernamePasswordAuthenticationToken`, and calls the `AuthenticationManager`. On success it stores the authenticated `Authentication` (via a `SecurityContextRepository`, which persists it for the *next* request) and invokes success handlers; on failure it triggers failure handlers. Siblings at nearby positions include `BasicAuthenticationFilter` and `BearerTokenAuthenticationFilter`. ### 4. `ExceptionTranslationFilter` (just before authorization) Its entire body is essentially `try { chain.doFilter(...) } catch (...)`. It only sees exceptions from filters **after** it. It handles exactly two Spring Security exception types: - **`AuthenticationException`** -> start authentication: invoke the `AuthenticationEntryPoint` (e.g. redirect to `/login`, or send `401` with a `WWW-Authenticate` header). - **`AccessDeniedException`** -> if the user is anonymous, treat like authentication needed (send to login); otherwise invoke the `AccessDeniedHandler` (default: `403 Forbidden`). It also saves the request (via `RequestCache`) so the user returns to the original URL after logging in. ### 5. `AuthorizationFilter` (last) Spring Security 6's authorization decision point, which **replaced `FilterSecurityInterceptor`**. It consults an `AuthorizationManager` (built from your `authorizeHttpRequests { ... }` rules) and, if access is denied, throws `AccessDeniedException` — which propagates *up* to `ExceptionTranslationFilter`. It runs last so the decision is made with full identity information and just before the request would reach your controller. ## Why this exact order - **Load identity first** (SecurityContextHolderFilter) so everything can read it. - **CSRF early** to reject forged mutations cheaply. - **Authenticate in the middle**, populating identity for later. - **Wrap the tail** (ExceptionTranslationFilter) so it can catch both authn-needed and access-denied outcomes. - **Authorize last**, throwing into the just-installed try/catch. ## Common gotchas - If a custom filter throws an `AccessDeniedException` **before** `ExceptionTranslationFilter`, it will *not* be translated into a clean 403 — you'll get a raw 500. Place such filters after it or handle the exception yourself. - Disabling CSRF removes `CsrfFilter` entirely (common for stateless token APIs). - `AuthorizationFilter` vs the old `FilterSecurityInterceptor`: same slot, new API (`AuthorizationManager` instead of `AccessDecisionManager`/voters).
- What changed between SecurityContextPersistenceFilter (SS5) and SecurityContextHolderFilter (SS6)?The new filter only *loads* the context (lazily via a Supplier) and never eagerly creates/saves an empty one. Saving the context is now the authentication mechanism's job, avoiding needless empty-session creation.
- AuthorizationFilter throws AccessDeniedException. Which filter turns that into a 403, and how does it reach it?ExceptionTranslationFilter, which sits immediately before AuthorizationFilter and wraps the downstream chain in try/catch. The exception propagates up the call stack into that catch block; for an authenticated user it invokes the AccessDeniedHandler (403).
saying these in an interview costs you the question
- Placing AuthorizationFilter before authentication filters.
- Claiming ExceptionTranslationFilter catches exceptions from filters before it.
- Saying CsrfFilter validates GET/HEAD requests (it only guards mutating verbs).
- Thinking SecurityContextHolderFilter still eagerly creates an empty context like the old persistence filter.