Skip to content
Ashish.
All posts
Diagram illustrating the Spring Security FilterChain architecture and data flow.

Spring Security: Filters, Chains & Auth

An examination of Spring Security architecture covering the SecurityFilterChain, filter mechanisms, and the internal authentication flow.

By Ashish SrivastavaPart 2 of Spring Security Deep Dive Series

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.

  1. Request Arrival: The DelegatingFilterProxy receives the request and passes it to the FilterChainProxy.
  2. Context Initialization: The first filter, SecurityContextPersistenceFilter, checks if there is an existing session. Finding none, it creates a new SecurityContext and attaches it to the SecurityContextHolder. It then calls doFilter on the next filter.
  3. Credential Extraction: The request reaches BasicAuthenticationFilter. Note that BasicAuthenticationFilter extends UsernamePasswordAuthenticationFilter, making it the specific implementation used for Basic Auth scenarios. This filter inspects the Authorization header, detects the Basic scheme, and decodes the base64 string to extract the username ("alice") and password ("secret"). It constructs an Authentication object: UsernamePasswordAuthenticationToken("alice", "secret", ...) and stores this object in the SecurityContext as an unauthenticated token.
  4. Validation: The request proceeds to the AuthenticationManager. This is not a filter itself but a component invoked by the filter. The BasicAuthenticationFilter delegates the token to the AuthenticationManager.
  5. User Lookup: The AuthenticationManager (typically a ProviderManager) iterates through registered AuthenticationProviders. The DaoAuthenticationProvider is selected. It queries the UserDetailsService (configured to load user details from a database) for "alice".
  6. Verification: The provider retrieves the stored password hash. It uses a PasswordEncoder to compare the raw password "secret" against the stored hash. If they match, the provider returns a new Authentication object with the authenticated flag set to true and the Principal populated with the user's roles and authorities.
  7. Context Update: The BasicAuthenticationFilter updates the SecurityContext with this fully authenticated Authentication object.

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

  1. Filter Ordering Risks: Placing FilterSecurityInterceptor before UsernamePasswordAuthenticationFilter causes 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.
  2. 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.
  3. Improper Exception Handling: Failing to configure ExceptionTranslationFilter correctly 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 SecurityFilterChain not 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 BasicAuthenticationFilter relative to FilterSecurityInterceptor is the most critical dependency.
  • State is Thread-Local: Remember that the SecurityContext is stored in a ThreadLocal. 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