Careeroza
The Security Filter Chain
Filters in Order
A typical chain with form login and HTTP Basic contains these filters, in this order. Order matters: the user must be authenticated before authorization is checked.
| Filter | What it does |
|---|---|
SecurityContextHolderFilter | loads the user from the session (if any) into the SecurityContextHolder |
HeaderWriterFilter | adds security headers (X-Frame-Options, HSTS...) |
CorsFilter | answers CORS preflight requests |
CsrfFilter | rejects POST/PUT/DELETE without a valid CSRF token |
LogoutFilter | handles POST /logout |
UsernamePasswordAuthenticationFilter | handles POST /login form submissions |
BasicAuthenticationFilter | reads the Authorization: Basic ... header |
AnonymousAuthenticationFilter | if nobody logged in, sets an "anonymous" user |
ExceptionTranslationFilter | turns security exceptions into 401 (or redirect to login) and 403 |
AuthorizationFilter | checks your authorizeHttpRequests rules; the last gate |
A Toy Filter Chain
Example
The real chain follows the same pattern as this plain-Java version: each filter either stops the request or calls chain.next():
import java.util.*;
public class Main {
record Request(String method, String path, String authHeader) {}
interface Filter { String doFilter(Request req, Chain chain); }
static class Chain {
private final List<Filter> filters;
private int index = 0;
Chain(List<Filter> filters) { this.filters = filters; }
String next(Request req) {
if (index < filters.size()) return filters.get(index++).doFilter(req, this);
return "200 OK from controller " + req.path(); // end of chain = DispatcherServlet
}
}
static String user; // our tiny "SecurityContext"
public static void main(String[] args) {
Filter logging = (req, chain) -> { System.out.println(" [log] " + req.method() + " " + req.path()); return chain.next(req); };
Filter basicAuth = (req, chain) -> {
if ("Basic YXNoYTphc2hhMTIz".equals(req.authHeader())) user = "asha";
return chain.next(req);
};
Filter authorization = (req, chain) -> {
if (req.path().startsWith("/api/products")) return chain.next(req); // permitAll
if (user == null) return "401 Unauthorized"; // authenticated()
return chain.next(req);
};
List<Filter> filters = List.of(logging, basicAuth, authorization);
Request[] requests = {
new Request("GET", "/api/products", null),
new Request("GET", "/api/cart", null),
new Request("GET", "/api/cart", "Basic YXNoYTphc2hhMTIz"),
};
for (Request r : requests) {
user = null; // cleared after every request
System.out.println(new Chain(filters).next(r));
}
}
}Output
[log] GET /api/products 200 OK from controller /api/products [log] GET /api/cart 401 Unauthorized [log] GET /api/cart 200 OK from controller /api/cart
See Your Real Chain
Debug Logging
Turn on debug logging to print the filters Spring built:
application.properties
logging.level.org.springframework.security=DEBUGExample output
DEBUG ... DefaultSecurityFilterChain : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter DEBUG ... FilterChainProxy : Securing GET /api/cart DEBUG ... AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext DEBUG ... ExceptionTranslationFilter : Sending AnonymousAuthenticationToken [Principal=anonymousUser, ...] to authentication entry point since access is denied
Tip: When a request fails with an unexpected 401/403, DEBUG logging for org.springframework.security tells you exactly which filter rejected it. Use it only in development; it is very noisy.