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.

FilterWhat it does
SecurityContextHolderFilterloads the user from the session (if any) into the SecurityContextHolder
HeaderWriterFilteradds security headers (X-Frame-Options, HSTS...)
CorsFilteranswers CORS preflight requests
CsrfFilterrejects POST/PUT/DELETE without a valid CSRF token
LogoutFilterhandles POST /logout
UsernamePasswordAuthenticationFilterhandles POST /login form submissions
BasicAuthenticationFilterreads the Authorization: Basic ... header
AnonymousAuthenticationFilterif nobody logged in, sets an "anonymous" user
ExceptionTranslationFilterturns security exceptions into 401 (or redirect to login) and 403
AuthorizationFilterchecks 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=DEBUG

Example 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.