The Security Filter Chain: Every Filter Explained

Every Filter Has a Job

The Spring Security filter chain is not a monolith — it’s 15-20 individual filters, each responsible for exactly one concern. Understanding each filter’s job means you can:

  • Know exactly where in the chain a request fails
  • Add your own filter in the right position
  • Remove filters you don’t need for performance
  • Debug authentication and authorization problems

The Complete Filter Order

Spring Security defines a strict ordering for its filters. Here is the full chain in execution order:

flowchart TD
    R[HTTP Request] --> F1
    F1["`**1. DisableEncodeUrlFilter**
    Prevents session ID appended to URLs`"] --> F2
    F2["`**2. WebAsyncManagerIntegrationFilter**
    Propagates SecurityContext to async threads`"] --> F3
    F3["`**3. SecurityContextHolderFilter**
    Loads/saves SecurityContext from repository`"] --> F4
    F4["`**4. HeaderWriterFilter**
    Writes security response headers (CSP, HSTS, etc)`"] --> F5
    F5["`**5. CorsFilter**
    Handles CORS preflight and headers`"] --> F6
    F6["`**6. CsrfFilter**
    Validates CSRF token on state-changing requests`"] --> F7
    F7["`**7. LogoutFilter**
    Handles logout requests`"] --> F8
    F8["`**8. UsernamePasswordAuthenticationFilter**
    Processes POST /login form submissions`"] --> F9
    F9["`**9. DefaultLoginPageGeneratingFilter**
    Generates the built-in login HTML page`"] --> F10
    F10["`**10. DefaultLogoutPageGeneratingFilter**
    Generates the built-in logout HTML page`"] --> F11
    F11["`**11. BasicAuthenticationFilter**
    Processes Authorization: Basic header`"] --> F12
    F12["`**12. RequestCacheAwareFilter**
    Restores saved request after authentication`"] --> F13
    F13["`**13. SecurityContextHolderAwareRequestFilter**
    Wraps request with security-aware methods`"] --> F14
    F14["`**14. AnonymousAuthenticationFilter**
    Sets anonymous Authentication if none set`"] --> F15
    F15["`**15. SessionManagementFilter**
    Session fixation, concurrent session control`"] --> F16
    F16["`**16. ExceptionTranslationFilter**
    Converts security exceptions to HTTP responses`"] --> F17
    F17["`**17. AuthorizationFilter**
    Final access decision: allow or deny`"] --> DS
    DS[DispatcherServlet → Controller]

Not all filters are active by default. Each filter is added only when you configure the corresponding feature (e.g., BearerTokenAuthenticationFilter is added only when you configure OAuth2 resource server).


Filter-by-Filter Breakdown

1. DisableEncodeUrlFilter

Prevents the Servlet container from appending ;jsessionid=... to URLs. Session IDs in URLs are a security risk — they can be leaked via Referer headers or browser history.

// Without this filter, redirect URLs could look like:
https://example.com/home;jsessionid=abc123xyz
// That session ID is now in browser history, server logs, etc.

Always active. No configuration needed.


2. WebAsyncManagerIntegrationFilter

Spring MVC supports async request handling (DeferredResult, Callable). Async processing runs on a different thread — which means the ThreadLocal-based SecurityContextHolder would lose the authenticated user.

This filter sets up SecurityContextCallableProcessingInterceptor so that the SecurityContext is propagated to the async thread.


3. SecurityContextHolderFilter

The most important bookkeeping filter:

  • Before the chain: loads the SecurityContext from SecurityContextRepository (HTTP session or request attribute) and sets it in SecurityContextHolder
  • After the chain: saves the updated SecurityContext back to SecurityContextRepository and clears SecurityContextHolder
sequenceDiagram
    participant Filter as SecurityContextHolderFilter
    participant Repo as SecurityContextRepository
    participant Holder as SecurityContextHolder
    participant Chain as Rest of Filter Chain

    Filter->>Repo: load(request)
    Repo-->>Filter: SecurityContext (or empty)
    Filter->>Holder: setContext(securityContext)
    Filter->>Chain: doFilter(request, response)
    Chain-->>Filter: return
    Filter->>Repo: save(securityContext, request, response)
    Filter->>Holder: clearContext()

For stateless JWT APIs, the SecurityContextRepository is RequestAttributeSecurityContextRepository — the context is not saved to the session between requests.


4. HeaderWriterFilter

Writes security-related HTTP response headers. Runs before authentication so that security headers are always present, even on error responses:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • X-XSS-Protection: 0 (modern browsers ignore this, CSP is preferred)

Configured via http.headers(...). Covered in detail in Article 25.


5. CorsFilter

Handles Cross-Origin Resource Sharing (CORS). Processes preflight OPTIONS requests and adds CORS headers to responses. Runs early in the chain so that CORS preflight requests are handled even for protected endpoints (the CORS preflight itself should not require authentication).

Configured via http.cors(...). Covered in Article 24.


6. CsrfFilter

Validates CSRF tokens on state-changing requests (POST, PUT, PATCH, DELETE). Reads the expected token from CsrfTokenRepository and compares it to the token in the request (header or parameter).

If the token is missing or wrong, the request is rejected with 403 before reaching any authentication filter. Only added when CSRF protection is enabled (default: enabled for web apps, commonly disabled for stateless REST APIs).

Covered in Article 23.


7. LogoutFilter

Intercepts requests to the logout URL (/logout by default). On match:

  1. Runs registered LogoutHandler instances (clears session, cookies, SecurityContextHolder)
  2. Calls LogoutSuccessHandler (redirects to /login?logout by default)
http.logout(logout -> logout
    .logoutUrl("/api/auth/logout")
    .logoutSuccessHandler((request, response, auth) -> {
        response.setStatus(HttpStatus.OK.value());
    })
    .invalidateHttpSession(true)
    .clearAuthentication(true)
    .deleteCookies("JSESSIONID")
);

8. UsernamePasswordAuthenticationFilter

Processes form login requests: POST /login with username and password parameters.

sequenceDiagram
    participant Client
    participant F as UsernamePasswordAuthFilter
    participant AM as AuthenticationManager
    participant SH as SuccessHandler / FailureHandler

    Client->>F: POST /login {username, password}
    F->>F: attemptAuthentication()
    F->>AM: authenticate(UsernamePasswordAuthToken)
    AM-->>F: Authentication (success) or exception (failure)
    alt Authentication success
        F->>SH: onAuthenticationSuccess()
        SH-->>Client: Redirect to / or return 200
    else Authentication failure
        F->>SH: onAuthenticationFailure()
        SH-->>Client: Redirect to /login?error
    end

Only present when http.formLogin(...) is configured.


9 & 10. DefaultLoginPageGeneratingFilter / DefaultLogoutPageGeneratingFilter

When you call http.formLogin(Customizer.withDefaults()) without specifying a custom login page, Spring Security generates one. DefaultLoginPageGeneratingFilter intercepts GET /login and returns a simple HTML login form.

Once you provide loginPage("/my-login"), these filters are removed from the chain.


11. BasicAuthenticationFilter

Processes HTTP Basic authentication: reads the Authorization: Basic base64(username:password) header. Decodes it, creates a UsernamePasswordAuthenticationToken, and authenticates it.

Only present when http.httpBasic(...) is configured.

Authorization: Basic YWxpY2U6c2VjcmV0
                      ↑ base64("alice:secret")

12. RequestCacheAwareFilter

Before authentication, when a user tries to access a protected resource, Spring Security saves the original request in a RequestCache and redirects to login. After successful authentication, RequestCacheAwareFilter restores the saved request so the user lands on the originally-requested page.


13. SecurityContextHolderAwareRequestFilter

Wraps the HttpServletRequest with a SecurityContextHolderAwareRequestWrapper. This makes Servlet security API methods work correctly:

  • request.isUserInRole("ADMIN") → checks Spring Security authorities
  • request.getUserPrincipal() → returns the Spring Security Authentication
  • request.authenticate(response) → triggers authentication

14. AnonymousAuthenticationFilter

If no Authentication has been set in SecurityContextHolder by this point in the chain (no login header, no session, no JWT), this filter sets an AnonymousAuthenticationToken:

// Sets anonymous authentication so SecurityContext is never null
SecurityContextHolder.getContext().setAuthentication(
    new AnonymousAuthenticationToken("key", "anonymousUser", 
        List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")))
);

This ensures SecurityContextHolder.getContext().getAuthentication() always returns a non-null value. You can check for anonymous users with isAnonymous() in SpEL expressions.


15. SessionManagementFilter

Handles session-related security concerns:

  • Session fixation protection: creates a new session ID after successful authentication (prevents session fixation attacks where an attacker pre-sets a session ID)
  • Concurrent session control: enforces maximum concurrent sessions per user
  • Session creation policy: enforces the configured policy (ALWAYS, IF_REQUIRED, NEVER, STATELESS)

16. ExceptionTranslationFilter

Translates Spring Security exceptions into HTTP responses. This filter wraps the rest of the chain and catches two types of exceptions:

flowchart TD
    E[Exception from AuthorizationFilter] --> ETF[ExceptionTranslationFilter]
    ETF -->|AuthenticationException\nnot logged in| AEP[AuthenticationEntryPoint\n→ 401 or redirect to login]
    ETF -->|AccessDeniedException\nlogged in but no permission| ADP[AccessDeniedHandler\n→ 403]
// Customize the entry point and denied handler
http.exceptionHandling(ex -> ex
    .authenticationEntryPoint((request, response, authException) -> {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json");
        response.getWriter().write("{\"error\": \"Authentication required\"}");
    })
    .accessDeniedHandler((request, response, accessDeniedException) -> {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().write("{\"error\": \"Access denied\"}");
    })
);

17. AuthorizationFilter

The final filter — makes the access control decision. It consults the AuthorizationManager which evaluates the rules defined in authorizeHttpRequests():

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/public/**").permitAll()
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.GET, "/products/**").hasAnyRole("USER", "ADMIN")
    .anyRequest().authenticated()
);

If the AuthorizationManager denies the request, AuthorizationFilter throws AccessDeniedException, which ExceptionTranslationFilter (one step earlier) catches and converts to a 403 response.


Custom Filters: Where to Add Them

When you add a custom filter (e.g., a JWT validation filter), you must specify where in the chain it belongs. Spring Security provides three insertion methods:

http
    .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
    // Runs BEFORE UsernamePasswordAuthenticationFilter
    // Use for: JWT, API key, custom token filters

    .addFilterAfter(myAuditFilter, AuthorizationFilter.class)
    // Runs AFTER AuthorizationFilter
    // Use for: post-authorization audit logging

    .addFilterAt(myCustomFilter, BasicAuthenticationFilter.class)
    // Runs AT THE SAME POSITION as BasicAuthenticationFilter (replaces order slot)
    // Use for: completely replacing a built-in filter

Custom JWT Filter Example

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response); // no JWT, pass through
            return;
        }

        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}
// Register in SecurityFilterChain
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

OncePerRequestFilter guarantees the filter runs exactly once per request — even with async dispatch or error dispatch, which can re-run the filter chain.


Debugging the Filter Chain

See Which Filters Are Active

Enable TRACE logging to see every filter executing on each request:

logging:
  level:
    org.springframework.security: TRACE

Output (abbreviated):

TRACE o.s.security.web.FilterChainProxy - Securing GET /api/products
TRACE o.s.s.w.c.SecurityContextHolderFilter - Set SecurityContextHolder to empty SecurityContext
TRACE o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
TRACE o.s.s.w.a.a.filter.AuthorizationFilter - Authorized filter invocation [GET /api/products] with authorization rule PermitAllAuthorizationManager
@Component
public class FilterChainDiagnostics implements ApplicationRunner {

    @Autowired
    private FilterChainProxy filterChainProxy;

    @Override
    public void run(ApplicationArguments args) {
        filterChainProxy.getFilterChains().forEach(chain -> {
            System.out.println("Chain: " + chain);
            ((List<?>) ((DefaultSecurityFilterChain) chain).getFilters())
                .forEach(f -> System.out.println("  → " + f.getClass().getSimpleName()));
        });
    }
}

Sample output:

Chain: DefaultSecurityFilterChain [...]
  → DisableEncodeUrlFilter
  → WebAsyncManagerIntegrationFilter
  → SecurityContextHolderFilter
  → HeaderWriterFilter
  → CorsFilter
  → CsrfFilter
  → LogoutFilter
  → JwtAuthenticationFilter          ← your custom filter
  → UsernamePasswordAuthenticationFilter
  → AnonymousAuthenticationFilter
  → ExceptionTranslationFilter
  → AuthorizationFilter

Multiple Security Filter Chains

Different URL patterns can have completely different security configurations:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // Chain 1: REST API — stateless, JWT
    @Bean
    @Order(1)
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")  // only matches /api/**
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    // Chain 2: Web UI — stateful, form login
    @Bean
    @Order(2)
    public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

@Order determines which chain is evaluated first. securityMatcher() restricts which requests the chain applies to. A request matching /api/products uses Chain 1; a request matching /admin/dashboard falls through to Chain 2.


Summary

  • The Spring Security filter chain is an ordered list of Servlet filters, each responsible for one concern.
  • Key filters: SecurityContextHolderFilter (context lifecycle), UsernamePasswordAuthenticationFilter (form login), AnonymousAuthenticationFilter (anonymous identity), ExceptionTranslationFilter (exception → HTTP response), AuthorizationFilter (access control decision).
  • Add custom filters with addFilterBefore(), addFilterAfter(), or addFilterAt(). Use OncePerRequestFilter as the base class.
  • Debug the active filter list with TRACE logging or by injecting FilterChainProxy.
  • Multiple SecurityFilterChain beans (with @Order and securityMatcher()) support different security models for different URL namespaces.

Next: Article 3 dives into the SecurityContext and Authentication object — understanding exactly what gets stored, how it flows through threads, and how to access the current user anywhere in your code.