Intermediate · Secure

Trace a Request Through the Security Filter Chain

Understand chain matching, authentication filters, exception translation, and `AuthorizationFilter` ordering.

Primary source: Spring Security 6.5 Reference Architecture

50 min

Lesson 1 of 5

1d streak

The Architectural Question

Requests fail closed with mysterious 401s or 403s when nobody on the team knows which chain matched or which filter rejected them.

What this guide proves

  • Define component-based `SecurityFilterChain` beans
  • Inspect the active filters for one request
  • Place custom filters intentionally rather than guessing an order

The Spring Security Filter Chain: From Servlet to Context

Spring Security's web layer is built on standard Java Servlet Filters. When an HTTP request enters the Servlet container (such as Tomcat), the standard bridge filter DelegatingFilterProxy intercepts it and delegates processing to a Spring ApplicationContext singleton bean named springSecurityFilterChain. This bean is an instance of FilterChainProxy, which maintains a list of SecurityFilterChain beans. FilterChainProxy performs chain selection (matching the request path and headers against each chain's RequestMatcher in sequence), then routes the request through that matching chain's deterministic list of security filters.

  1. 011. SecurityContextHolderFilter: Restores the SecurityContext from the SecurityContextRepository (e.g. HttpSession or request cache)
  2. 022. HeaderWriterFilter: Injects secure response headers (X-Content-Type-Options, X-Frame-Options, HSTS)
  3. 033. CorsFilter: Handles cross-origin preflight OPTIONS requests and sets Access-Control headers
  4. 044. CsrfFilter: Validates CSRF tokens on mutating HTTP methods (POST, PUT, DELETE, PATCH)
  5. 055. AuthenticationFilter: (e.g. BearerTokenAuthenticationFilter or UsernamePasswordAuthenticationFilter) Validates credentials and constructs an Authentication token
  6. 066. ExceptionTranslationFilter: Catches AuthenticationException (delegating to the configured AuthenticationEntryPoint, such as 401 for REST or redirect for login) and AccessDeniedException (delegating to AccessDeniedHandler to return 403 Forbidden)
  7. 077. AuthorizationFilter: Evaluates authorization rules against the current request and principal authorities via RequestMatcherDelegatingAuthorizationManager

The Single Entry Point

All security filtering occurs before the request ever touches Spring MVC's DispatcherServlet or your @RestController. If authentication or authorization fails, the filter chain halts execution immediately and generates the error response.

Active Recall Checkpoint

Where in the filter chain is the SecurityContext created and associated with the current executing thread?

Hint: Which filter runs earliest to restore previous session state?

Show answer & explanation

SecurityContextHolderFilter runs near the top of the chain, retrieving the SecurityContext from storage (or creating an empty one) and setting it in SecurityContextHolder for downstream filters and controllers.

Spring Security 6+ Modern Lambda DSL Configuration

Spring Security 6 (included in Spring Boot 3+) completely removed the legacy WebSecurityConfigurerAdapter and deprecated fluent chaining methods like .and(). All security configuration is now expressed via lambda DSLs returning a SecurityFilterChain bean.

SecurityConfig.java
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Safe to disable ONLY when authenticating exclusively via non-ambient credentials (Authorization header);
            // ambient credentials (cookies, browser-cached basic auth) require CSRF protection regardless of statelessness.
            .csrf(csrf -> csrf.disable())
            .cors(Customizer.withDefaults())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return http.build();
    }
}

The Lambda DSL Advantage

The lambda DSL makes configuration scope visually explicit. Opening a lambda block csrf(csrf -> ...) scopes configuration options to that specific filter without leaking state to sibling components.

Active Recall Checkpoint

Why was WebSecurityConfigurerAdapter deprecated and removed in modern Spring Security?

Hint: How does component-based bean configuration compare to class inheritance?

Show answer & explanation

Inheriting from a base adapter class forced tight coupling and rigid class hierarchies. Exposing a @Bean SecurityFilterChain promotes component-based composition, multiple independent filter chains, and simpler testing.

Request Matcher Ordering: Order of Precedence & Common Pitfalls

In authorizeHttpRequests, request matchers are evaluated in first-match-wins order. Once a request matches a pattern, Spring Security applies that rule immediately and stops evaluating subsequent matchers.

Configuration OrderPattern EvaluatedResult for GET /api/admin/users
INCORRECT ORDER (Broad first)1. /api/ -> authenticated() 2. /api/admin/ -> hasRole("ADMIN")VULNERABILITY: Any authenticated regular user matches rule 1 and gains access to the admin endpoint!
CORRECT ORDER (Specific first)1. /api/admin/ -> hasRole("ADMIN") 2. /api/ -> authenticated()SECURE: Admin route hits rule 1 first and strictly enforces the ADMIN role.

Chain Authorization vs. Global Default Deny

In Spring Security 6+ with authorizeHttpRequests, RequestMatcherDelegatingAuthorizationManager evaluates matchers sequentially. If an incoming request reaches the end of the chain without matching any rule, the default decision is to DENY access (throwing AuthorizationDeniedException / 403 Forbidden). Ending your chain with an explicit .anyRequest().authenticated() or .anyRequest().denyAll() makes this security posture intentional and avoids ambiguity when new routes are added.

Active Recall Checkpoint

What happens if you configure additional matchers after calling .anyRequest().authenticated() in an authorizeHttpRequests block?

Hint: Does Spring Security allow matchers after anyRequest()?

Show answer & explanation

Spring Security rejects the configuration at application startup! In AbstractRequestMatcherRegistry, configuring any matcher after anyRequest() invokes Assert.state(!this.anyRequestConfigured, "Can't configure mappers after anyRequest"), which throws an IllegalStateException. In contrast, if you order rules broad-to-specific before anyRequest() (such as .requestMatchers("/**").authenticated() before a specific /public/** rule), the application starts without throwing an exception, but the broader rule matches first and silently shadows the specific rule.

Deliberate Failure Lab: Accidental Public Route Bypass

Let us analyze a real-world security vulnerability caused by regex / wildcard misconfigurations:

FlawedSecurityConfig.java
auth
    .requestMatchers("/api/public/**").permitAll()
    .requestMatchers("/api/public/admin-debug").hasRole("ADMIN") // DEAD CODE! Shadowed by line above!
    .anyRequest().authenticated();

The Security Breach

An unauthenticated external attacker requests GET /api/public/admin-debug. Because /api/public/** matches first, Spring Security permits the request with 200 OK. The admin restriction is completely bypassed.

Resolution

Move the more specific rule /api/public/admin-debug above /api/public/**. Always write integration tests that assert unauthenticated access to sensitive endpoints returns 401/403.

Active Recall Checkpoint

How do you write a test verifying that an unauthenticated user cannot access /api/public/admin-debug?

Hint: Use MockMvc with get(...) without credentials.

Show answer & explanation

Use mockMvc.perform(get("/api/public/admin-debug")).andExpect(status().isUnauthorized()) (or isForbidden()). Automated tests catch rule shadowing immediately.

Runtime Architecture & Flow

SecurityFilterChain Pipeline

Spring Security Filter Chain Simulator

HTTP REQUESTPOST /api/orders (Authorization: Bearer valid.jwt.token, Role: ROLE_ADMIN)
STATUS: 200
Filter 01
CorsFilter

Validates Origin header & handles HTTP OPTIONS preflight

Filter 02
SecurityContextHolderFilter

Binds SecurityContext to ThreadLocal execution context

Filter 03
JwtAuthenticationFilter

Validates cryptographic signature & extracts claims

Filter 04
ExceptionTranslationFilter

Translates Spring Security exceptions into 401/403

Filter 05
AuthorizationFilter

Evaluates required authorities (e.g. ROLE_ADMIN)

Filter 06
DispatcherServlet

Dispatches validated request to @RestController

Security Outcome Analysis

200 OK - Request reached OrderController.createOrder()

The JWT signature was verified, claims were populated into an Authentication token, and authorization passed for ROLE_ADMIN. Full chain completed successfully.

Synthesis & Production Verification

Production Acceptance Criteria

  • Define a SecurityFilterChain bean with ordered public and protected matchers
  • Dump and read the applied filter list for one real request
  • Insert a custom filter at a deliberate position and demonstrate its effect

Architectural & Technical Review

Core Interview Questions

Q01How does DelegatingFilterProxy bridge servlet containers to Spring-managed security filters?Show answer

DelegatingFilterProxy is a standard servlet filter registered in the container that delegates doFilter calls to the FilterChainProxy Spring bean, which executes the active SecurityFilterChain.

Q02What are the key advantages of Spring Security 6 lambda DSL over legacy adapters?Show answer

Removes deprecated WebSecurityConfigurerAdapter and .and() chaining, promotes composition of SecurityFilterChain beans, scopes configuration cleanly within lambdas, and supports multiple chains.

Q03Why does order matter in authorizeHttpRequests request matchers?Show answer

Request matchers evaluate in first-match-wins order. Specific patterns must be listed before general patterns; placing broad patterns first shadows specific rules and can open security holes.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Placing general matchers above specific routes causing permission bypasses — First-match-wins rules mean a general permitAll rule above a specific admin route will allow unauthorized access.
  • Pitfall: Calling matchers after anyRequest() or placing broad matchers first — Calling requestMatchers() after anyRequest() fails startup with IllegalArgumentException. Placing broad patterns like /** before specific rules matches prematurely and accidentally permits access.

Primary Documentation & Specifications

Supplementary Module Resources