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
Lesson 1 of 5
1d streakThe 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.
- 011. SecurityContextHolderFilter: Restores the SecurityContext from the SecurityContextRepository (e.g. HttpSession or request cache)
- 022. HeaderWriterFilter: Injects secure response headers (X-Content-Type-Options, X-Frame-Options, HSTS)
- 033. CorsFilter: Handles cross-origin preflight OPTIONS requests and sets Access-Control headers
- 044. CsrfFilter: Validates CSRF tokens on mutating HTTP methods (POST, PUT, DELETE, PATCH)
- 055. AuthenticationFilter: (e.g. BearerTokenAuthenticationFilter or UsernamePasswordAuthenticationFilter) Validates credentials and constructs an Authentication token
- 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)
- 077. AuthorizationFilter: Evaluates authorization rules against the current request and principal authorities via RequestMatcherDelegatingAuthorizationManager
The Single Entry Point
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.
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
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 Order | Pattern Evaluated | Result 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
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:
auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/public/admin-debug").hasRole("ADMIN") // DEAD CODE! Shadowed by line above!
.anyRequest().authenticated();The Security Breach
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
/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
CorsFilter
Validates Origin header & handles HTTP OPTIONS preflight
SecurityContextHolderFilter
Binds SecurityContext to ThreadLocal execution context
JwtAuthenticationFilter
Validates cryptographic signature & extracts claims
ExceptionTranslationFilter
Translates Spring Security exceptions into 401/403
AuthorizationFilter
Evaluates required authorities (e.g. ROLE_ADMIN)
DispatcherServlet
Dispatches validated request to @RestController
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
Key Takeaway
Spring Security delegates request processing through a deterministic chain of servlet filters, requiring specific-before-general request matcher ordering and explicit authorization rules.
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
Lesson Primary References
Supplementary Module Resources