
The Spring Security Filter Chain, Ordered
An examination of the Spring Security filter chain, focusing on filter order, DelegatingFilterProxy, and servlet architecture for backend engineers.
Spring Security is often misunderstood as a black box that "just works." For backend engineers, this abstraction breaks down when debugging authentication failures, CORS issues, or unexpected redirect loops. To understand Spring Security, you must view it not as a single component, but as a stack of ordered javax.servlet.Filter (or jakarta.servlet.Filter) instances that intercept HTTP requests before they reach your controllers.
This article examines the mechanics of this filter chain, focusing on how DelegatingFilterProxy integrates with the servlet architecture, why filter order is non-negotiable, and how the chain processes a request from entry to exit.
The Entry Point: DelegatingFilterProxy
The servlet container (Tomcat, Jetty, etc.) does not know about Spring Beans. It only knows about Filter implementations registered in web.xml or via programmatic configuration. Spring Security solves this integration problem using DelegatingFilterProxy.
DelegatingFilterProxy is a Filter that delegates all operations to a Spring-managed bean. This indirection is crucial because it allows Spring Security filters to be initialized lazily after the Spring ApplicationContext is fully loaded. Without this proxy, filters would need to be instantiated by the servlet container before the Spring context existed, making dependency injection impossible. As noted in the official Spring Security reference guide, this proxy is the standard mechanism for integrating Spring-managed security components with the Java EE servlet specification.
// Programmatic registration example
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
// DelegatingFilterProxy is registered automatically under the name "springSecurityFilterChain"
}
}When a request arrives, the servlet container calls doFilter() on DelegatingFilterProxy. The proxy then looks up the bean named springSecurityFilterChain (the default name) from the Spring context and delegates the call to it. This bean is an instance of FilterChainProxy.
The Filter Chain Proxy
FilterChainProxy is the central component that manages the list of security filters. It does not perform security logic itself; instead, it iterates through a list of Filter instances, calling doFilter() on each one in sequence.
The order of filters in this chain is critical. Each filter may modify the request, response, or SecurityContext. If a filter expects a certain state to be present (e.g., an authenticated user), it must come after the filter that establishes that state (e.g., an authentication filter).
Spring Security provides a default order for its filters, but you can customize it. The default order is defined internally by Spring Security's FilterComparator class and ensures that critical security checks happen in a predictable sequence.
// Simplified representation of the default order
List<Filter> defaultFilters = Arrays.asList(
new SecurityContextPersistenceFilter(), // 1. Load/Save context
new UsernamePasswordAuthenticationFilter(), // 2. Authenticate
new ExceptionTranslationFilter(), // 3. Handle exceptions
new FilterSecurityInterceptor() // 4. Authorize
);Key Filters in Sequence
Let’s trace a typical request through the spring security filter chain with named actors and named artifacts.
1. SecurityContextPersistenceFilter
Actor: SecurityContextPersistenceFilter
Artifact: SecurityContext
This filter runs first in the effective sequence. It loads the SecurityContext from the session (or repository) and places it into the SecurityContextHolder (ThreadLocal). It does NOT copy it into request attributes. Subsequent filters access it via SecurityContextHolder.getContext(). This step is essential because subsequent filters need to access the current security context to make authorization decisions.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
SecurityContext previous = SecurityContextHolder.getContext();
SecurityContext ctx = contextRepository.loadContext(httpRequest);
SecurityContextHolder.setContext(ctx);
try {
chain.doFilter(req, res);
} finally {
SecurityContextHolder.setContext(previous);
contextRepository.saveContext(ctx, httpReq, httpResp);
}
}2. Authentication Filter
Actor: UsernamePasswordAuthenticationFilter
Artifact: Authentication object
If the request is an HTTP POST to /login, this filter extracts the username and password from the request body. It then attempts to authenticate the user by calling the AuthenticationManager. If successful, it creates an Authentication object containing the user’s details and authorities.
This Authentication object is stored in the SecurityContext. If authentication fails, an AuthenticationException is thrown, and the chain stops.
3. Authorization Filter
Actor: FilterSecurityInterceptor
Artifact: AccessDecisionVoter
Once the user is authenticated, this filter checks if the user has the required roles or permissions to access the requested resource. It uses AccessDecisionVoter instances to evaluate the request against the configured access rules.
If the user lacks permission, an AccessDeniedException is thrown. If the user is not authenticated at all, the filter typically redirects to the login page or returns a 401 Unauthorized response.
It is worth noting that while delegatingfilterproxy is the entry point, misconfiguring the delegatingfilterproxy bean name or order can lead to security bypasses, so always verify the configuration keys in your web.xml or initializer classes.
Debugging Order Issues
Incorrect filter order is a common source of bugs. Consider a scenario where you add a custom filter to log request parameters. If you insert this filter before the AuthenticationFilter, it will attempt to log parameters for unauthenticated requests, which may include sensitive data like passwords.
More critically, if you place a custom filter that modifies the request body after the AuthenticationFilter, the authentication filter will have already read and consumed the request stream. This leads to errors because the request body is no longer available for reading.
// Incorrect order: Custom filter after authentication
filterChain.addFilterBefore(new CustomLoggingFilter(), SecurityContextPersistenceFilter.class);
// This is safe, but if you add it after AuthenticationFilter, it may fail
// Correct order: Custom filter before authentication
filterChain.addFilterBefore(new CustomLoggingFilter(), UsernamePasswordAuthenticationFilter.class);To debug filter order, you can enable debug logging for org.springframework.security. This will output the filter chain and the order in which filters are executed.
logging.level.org.springframework.security=DEBUGConclusion
Spring Security’s power lies in its flexibility, but this flexibility comes with the responsibility of managing filter order. By understanding the role of DelegatingFilterProxy, the mechanics of FilterChainProxy, and the sequence of key filters, you can build more secure and debuggable applications. Always remember: the servlet container calls the filters in order, and each filter may change the state of the request. Respect the order.
Related posts
SecurityFilterChain in Spring Security 6
A technical walkthrough of configuring SecurityFilterChain in Spring Security 6 using the Lambda DSL and RequestMatchers for Java developers.
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 Attribute-Based Access Control (ABAC) with Spring Security
A technical guide on implementing attribute-based access control (ABAC) using Spring Security and Open Policy Agent for dynamic policy enforcement.