
Spring Security: Filters, Chains & Auth
An examination of Spring Security architecture covering the SecurityFilterChain, filter mechanisms, and the internal authentication flow.
Spring Security Architecture: Filters, Chains, and Authentication
In the standard Spring MVC lifecycle, a request flows from the Servlet Container to the DispatcherServlet. Spring Security intercepts this flow not by replacing the dispatcher, but by inserting itself as a Filter in the Servlet container's chain. This architecture relies on a specific mechanism: the SecurityFilterChain. This is a runtime-compiled sequence of OncePerRequestFilter instances acting as a pipeline. Each filter performs a specific duty—parsing credentials, validating tokens, or checking permissions—before passing control to the next filter or the target resource. Understanding this pipeline requires looking at how the container bootstraps the chain and how authentication state propagates through the thread context.
The Bootstrapping Mechanism
The entry point for Spring Security is the DelegatingFilterProxy. When the application starts, the Spring container registers this proxy in the Servlet container's configuration. The proxy does not contain security logic; it is merely a bridge. Its sole responsibility is to delegate the doFilter invocation to a bean defined in the Spring ApplicationContext named springSecurityFilterChain.
When a request hits DelegatingFilterProxy.doFilter, it looks up the springSecurityFilterChain bean. This bean is an instance of FilterChainProxy. The FilterChainProxy holds the actual logic: it iterates through a list of SecurityFilterChain objects. If multiple chains exist (e.g., one for /api/** and another for /login), it selects the first chain whose pattern matches the incoming request URL. Once selected, it delegates to the FilterChain within that object.
This delegation model is critical because it allows the chain to be reconfigured dynamically without restarting the server. The FilterChainProxy essentially wraps the entire Spring Security logic, ensuring that every request is processed by the chain before it ever reaches the DispatcherServlet.
Constructing the Chain
The composition of the chain is driven by the HttpSecurity configuration object. When you write a configuration like http.authorizeHttpRequests().anyRequest().authenticated(), you are not just setting a rule; you are instructing the framework to build a specific graph of filters. The framework then calls http.build(), which triggers the SecurityFilterChainBuilder.
The builder creates a FilterChainProxy containing a list of filters. The order is deterministic and strictly enforced. For example, consider a standard authentication flow involving a username and password. The chain must include ChannelProcessingFilter (to handle SSL redirection) followed by SecurityContextPersistenceFilter (to manage session state), then UsernamePasswordAuthenticationFilter, and finally FilterSecurityInterceptor.
If UsernamePasswordAuthenticationFilter were placed after FilterSecurityInterceptor, the request would be rejected for lacking authorization before the credentials were ever parsed. The mechanism here is a linear dependency graph. The OncePerRequestFilter base class ensures that each filter executes exactly once per request, preventing infinite loops or duplicate processing if the filter chain is misconfigured.
// Conceptual representation of the compiled chain order
List<Filter> chain = Arrays.asList(
new ChannelProcessingFilter(), // Handles SSL
new SecurityContextPersistenceFilter(), // Loads context from session
new UsernamePasswordAuthenticationFilter(), // Parses credentials
new FilterSecurityInterceptor() // Checks access rules
);This compilation happens at application startup. The FilterChainProxy caches the compiled list of filters. When a request arrives, the proxy iterates through this cached list. If a filter throws an exception or completes its logic, the Chain.doFilter(next) method is invoked to pass the request to the subsequent filter.
The Authentication Pipeline
To visualize the mechanism, consider a request from a client named "Alice" attempting to access a protected endpoint /orders. Alice sends a POST request with a Basic Auth header containing alice:secret.
- Request Arrival: The
DelegatingFilterProxyreceives the request and passes it to theFilterChainProxy. - Context Initialization: The first filter,
SecurityContextPersistenceFilter, checks if there is an existing session. Finding none, it creates a newSecurityContextand attaches it to theSecurityContextHolder. It then callsdoFilteron the next filter. - Credential Extraction: The request reaches
BasicAuthenticationFilter. Note thatBasicAuthenticationFilterextendsUsernamePasswordAuthenticationFilter, making it the specific implementation used for Basic Auth scenarios. This filter inspects theAuthorizationheader, detects theBasicscheme, and decodes the base64 string to extract the username ("alice") and password ("secret"). It constructs anAuthenticationobject:UsernamePasswordAuthenticationToken("alice", "secret", ...)and stores this object in theSecurityContextas an unauthenticated token. - Validation: The request proceeds to the
AuthenticationManager. This is not a filter itself but a component invoked by the filter. TheBasicAuthenticationFilterdelegates the token to theAuthenticationManager. - User Lookup: The
AuthenticationManager(typically aProviderManager) iterates through registeredAuthenticationProviders. TheDaoAuthenticationProvideris selected. It queries theUserDetailsService(configured to load user details from a database) for "alice". - Verification: The provider retrieves the stored password hash. It uses a
PasswordEncoderto compare the raw password "secret" against the stored hash. If they match, the provider returns a newAuthenticationobject with theauthenticatedflag set totrueand thePrincipalpopulated with the user's roles and authorities. - Context Update: The
BasicAuthenticationFilterupdates theSecurityContextwith this fully authenticatedAuthenticationobject.
At this point, the request has been authenticated. The SecurityContext now holds the principal "Alice" with her authorities. The request continues down the chain to the FilterSecurityInterceptor.
Authorization and Context Propagation
The FilterSecurityInterceptor is the final gatekeeper before the request reaches the DispatcherServlet. It reads the SecurityContext to retrieve the current Authentication object. It then compares the user's authorities against the security metadata (defined in the @PreAuthorize annotations or HttpSecurity configuration) for the requested resource.
If Alice has the ROLE_ADMIN and the resource requires ROLE_ADMIN, the interceptor allows the request to proceed. If not, it throws an AccessDeniedException, which is caught by ExceptionTranslationFilter, resulting in a 403 response.
Crucially, the SecurityContext persists throughout this entire chain because it is bound to the current Thread. Spring Security uses SecurityContextHolder which defaults to a ThreadLocal storage strategy. This means the authenticated principal is available to any downstream component—Controllers, Services, or Repositories—without needing to pass the token explicitly as a method argument.
// Accessing the principal anywhere in the application
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // "alice"
Collection<GrantedAuthority> roles = auth.getAuthorities();This mechanism ensures that the security state is transparent to the business logic. The SecurityContext is also serialized to the HTTP session by SecurityContextPersistenceFilter at the end of the request, allowing subsequent requests to resume the session without re-authentication.
Understanding these Spring Security internals is vital for debugging why a request might fail at the filter level versus the service level.
FAQ
Q: Can I remove the SecurityContextPersistenceFilter?
A: You can, but it disables session-based state management. If removed, the SecurityContext will not be saved to the HTTP session, and every request will require re-authentication. This is generally only useful for stateless APIs using tokens, where you might rely on other mechanisms to pass the context.
Q: What happens if two filters try to modify the same request attribute?
A: Since filters execute sequentially, the last filter to write to an attribute wins. However, OncePerRequestFilter prevents multiple executions of the same filter instance. If multiple different filters manipulate the same attribute, it can lead to race conditions or unexpected behavior if they are not ordered correctly.
Q: Does SecurityFilterChain apply to static resources like CSS or JS?
A: By default, Spring Security applies to all requests. However, HttpSecurity configuration usually includes a rule like .requestMatchers("/css/**", "/js/**").permitAll() to bypass security for static resources. Without this, the chain would attempt to authenticate static file requests, which often results in errors or unnecessary overhead.
Common Pitfalls
- Filter Ordering Risks: Placing
FilterSecurityInterceptorbeforeUsernamePasswordAuthenticationFiltercauses every request to be denied immediately, as the system checks permissions before verifying who the user is. Always ensure credential extraction filters precede authorization filters. - Misconfigured SecurityContextPersistenceFilter: If this filter is disabled or misconfigured, the session state is lost between requests. This often leads to "session fixation" vulnerabilities or unexpected logout behavior where the user appears unauthenticated on subsequent pages.
- Improper Exception Handling: Failing to configure
ExceptionTranslationFiltercorrectly can result in raw stack traces being returned to the client instead of user-friendly error pages. Always ensure a custom error handler or default exception translation is in place for 401 and 403 errors.
Practical Takeaways
- The Chain is a Pipeline: Think of the
SecurityFilterChainnot as a list of rules, but as a water pipe where each filter is a valve. Water (the request) must pass through every valve in the exact specified order. - Order Matters More Than Content: A single misplaced filter can render the entire security configuration useless. The position of
BasicAuthenticationFilterrelative toFilterSecurityInterceptoris the most critical dependency. - State is Thread-Local: Remember that the
SecurityContextis stored in aThreadLocal. This means security state is isolated per thread, which is perfect for web requests but requires careful handling in asynchronous or multi-threaded service layers.
Conclusion
The power of Spring Security lies in this modular filter architecture. By decoupling the logic of authentication (extracting credentials) from authorization (checking permissions) and state management (session handling), the framework allows developers to swap components or reorder filters to suit specific needs. The SecurityFilterChain is not just a list; it is the execution engine of the security policy, driving the data flow from the raw HTTP request to a fully authenticated and authorized application context. Understanding the specific order of these filters and the mechanism of the SecurityContextHolder is essential for debugging complex security issues and optimizing performance in high-throughput applications.
Related posts
Implementing Passwordless MFA with FIDO2 and WebAuthn in Spring Boot
A technical walkthrough on integrating passwordless MFA using FIDO2 and WebAuthn within a Spring Boot application.
Building a Custom Authentication Provider in Spring Security
This article covers the implementation of a custom authentication mechanism within Spring Security using a dedicated AuthenticationProvider.
Implementing Account Lockout and Brute Force Protection in Spring Security
This article covers implementing account lockout and brute force protection mechanisms in Spring Security to secure failed logins with rate limiting and CAPTCHA.