Course Content
Spring Security Series
0/28
Spring Security

Spring Security Custom Filters: Mastering the Filter Chain for Robust Authentication & Authorization

Spring Security is an incredibly powerful and highly customizable authentication and access-control framework. For many applications, the default configuration provides a solid security foundation. However, as applications grow in complexity, relying solely on form-based login or basic authentication is often not enough. This is where the true power of Spring Security shines: its extensible filter chain. By creating custom filters, you can intercept incoming requests and implement sophisticated, tailored security logic that goes far beyond the out-of-the-box solutions.

This comprehensive guide will walk you through everything you need to know to master custom filters in Spring Security. We’ll explore why you need them, how the filter chain works, and provide a step-by-step tutorial on building and integrating a custom JWT (JSON Web Token) authentication filter. By the end, you’ll be equipped to build more robust, flexible, and secure Spring Boot applications.

Why Go Beyond the Defaults? The Case for Custom Filters

The default Spring Security filters are excellent for traditional, session-based web applications. But modern architectures, especially those involving microservices, single-page applications (SPAs), and third-party integrations, present unique security challenges. Custom filters are the perfect tool to address these scenarios.

  • JWT Token Authentication: For stateless APIs, validating a JWT from an Authorization header is the standard. A custom filter is the ideal place to parse the token, validate its signature and claims, and establish the user’s identity for the duration of the request.
  • API Key Validation: When providing access to third-party services, you might need to validate a static API key sent in a custom header (e.g., X-API-KEY). A custom filter can inspect the request for this key, validate it against a database, and grant or deny access accordingly.
  • Request Logging and Auditing: A custom filter can be used to create a detailed audit trail. It can log crucial information about every incoming request, such as the source IP, the requested endpoint, user details, and the time of the request, before it’s processed by your application logic.
  • Tenant Identification in Multi-Tenant Apps: In a multi-tenant system, you often need to identify the tenant early in the request lifecycle based on a subdomain, header, or request parameter. A custom filter can extract this tenant ID and store it in a context (like a ThreadLocal) for the rest of the application to use.

Understanding the Spring Security Filter Chain

At the heart of Spring Security is a concept called the FilterChainProxy. You can think of this as a special “master” filter that delegates incoming requests to a chain of other, more specialized servlet filters. Imagine an airport security checkpoint: you first go through an ID check, then your bags are x-rayed, then you might go through a metal detector. Each step is a distinct checkpoint with a single responsibility. The Spring Security filter chain works in the same way.

Each filter has a specific responsibility. For example, the UsernamePasswordAuthenticationFilter is responsible for handling credentials submitted from a login form, while the FilterSecurityInterceptor is responsible for the final authorization check, determining if the authenticated user has the right to access a specific URL. The order of these filters is critical. You must authenticate a user before you can authorize them, so the authentication filters must come before the authorization filters in the chain.

Anatomy of a Custom Filter: The Building Blocks

When creating a custom filter, you have a couple of options, but one is highly recommended for its robustness and simplicity within the Spring ecosystem.

Option 1: Implementing the `Filter` Interface

The most basic approach is to implement the standard Jakarta Servlet Filter interface. This requires you to implement three methods: init(), destroy(), and the core doFilter() method. While this works, it comes with a significant caveat: in a complex web application using features like request forwarding or error dispatching, your filter might be executed more than once for a single client request. This can lead to unexpected behavior and security vulnerabilities.

Option 2 (Recommended): Extending `OncePerRequestFilter`

This abstract class is your best friend when creating custom security filters. As its name implies, OncePerRequestFilter is a specialized base class provided by Spring that guarantees your filter logic will be executed only once per request. It elegantly handles the complexities of internal server forwards and dispatches, making your code cleaner and safer.

Instead of overriding doFilter(), you implement the abstract doFilterInternal() method. This is where you’ll place your core filter logic. It has the same signature and purpose as the original doFilter() method, but with the added guarantee of single execution.

Step-by-Step: Creating and Registering a Custom JWT Authentication Filter

Let’s put theory into practice by building the most common type of custom filter: one that handles JWT authentication for a stateless REST API.

Step 1: The Goal – A Simple JWT Validator

Our objective is to create a filter that performs the following actions on every incoming request:

  1. Check for an Authorization header with a “Bearer ” token.
  2. If a token exists, validate it. (For this example, our “validation” will be a simple check, but in a real app, this would involve checking the signature, expiration, and claims).
  3. If the token is valid, create an Authentication object and place it in the SecurityContextHolder. This tells Spring Security that the current user is authenticated.
  4. Allow the request to proceed down the filter chain to the controller.

Step 2: Create the Custom Filter Class

We’ll create a class called JwtAuthenticationFilter that extends OncePerRequestFilter.


import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;

@Component // Make it a Spring bean
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail; // In a real JWT, this would be the subject claim

        // 1. Check if the header is present and formatted correctly
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response); // Pass to the next filter
            return;
        }

        // 2. Extract the token from the header
        jwt = authHeader.substring(7);

        // 3. TODO: Validate the token (e.g., check signature, expiration)
        // For this example, we'll assume the token is valid if it's not empty.
        // In a real app, you would use a JWT library (like jjwt) to parse and validate.
        userEmail = "[email protected]"; // Placeholder for extracted username from token

        // 4. If the token is valid and the user is not yet authenticated
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // We don't have a password, so we use an empty list of authorities
            UserDetails userDetails = new User(userEmail, "", new ArrayList<>());

            // Create an Authentication object
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null, // Credentials
                    userDetails.getAuthorities()
            );

            authToken.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request)
            );

            // 5. Update the SecurityContextHolder
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }

        // 6. Pass the request along the filter chain
        filterChain.doFilter(request, response);
    }
}

Step 3: Register the Filter in the `SecurityFilterChain`

Creating the filter class is only half the battle. We now need to tell Spring Security to use it and, crucially, where to place it in the chain. The modern, component-based approach is to define a SecurityFilterChain bean in a @Configuration class.

We want our JWT filter to run before Spring’s standard UsernamePasswordAuthenticationFilter, as we are handling authentication ourselves for API requests.


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;

    // Inject our custom filter
    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // Disable CSRF for stateless APIs
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll() // Public endpoints
                        .anyRequest().authenticated() // All other requests need authentication
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Use stateless sessions
                // Here is the crucial part:
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Positioning is Everything: `addFilterBefore`, `addFilterAfter`, and `addFilterAt`

The order of your filter matters immensely. Adding a filter in the wrong place can break your security model or cause unexpected behavior. Spring Security provides three primary methods for positioning your custom filter within the chain.

  • addFilterBefore(customFilter, existingFilter.class): This is the most common and useful method. It places your custom filter immediately before a known, existing filter. In our example, we placed our `JwtAuthenticationFilter` before the `UsernamePasswordAuthenticationFilter` because we want to handle token-based auth before the chain attempts to process form-based auth.
  • addFilterAfter(customFilter, existingFilter.class): As you’d expect, this places your filter immediately after a known filter. This can be useful for tasks like response-header manipulation or logging that should happen after the main security logic has executed but before the response is committed.
  • addFilterAt(customFilter, existingFilter.class): This method replaces an existing filter in the chain with your custom one. This is less common but can be powerful if you need to completely override a default Spring Security behavior.

Common Pitfalls and Best Practices

As you work with custom filters, keep these common pitfalls and best practices in mind to avoid security holes and bugs.

  1. Forgetting `filterChain.doFilter(…)`: The most common mistake is forgetting to call `filterChain.doFilter(request, response)` at the end of your filter logic. If you omit this line, you effectively block the request. The filter chain will stop, and the request will never reach your controller.
  2. Blocking the Chain Prematurely: If authentication fails, you should handle it gracefully. Instead of letting an exception propagate, it’s often better to directly write an error response (e.g., HTTP 401 Unauthorized) and then simply `return` from the method, preventing the rest of the chain from executing.
  3. Incorrect Filter Ordering: Always think critically about where your filter belongs. An auditing filter might need to be very early in the chain to capture all requests, while a complex authorization filter must run after authentication is complete.
  4. Not Using `OncePerRequestFilter`: Forgetting to extend `OncePerRequestFilter` can lead to your filter logic running multiple times for a single request, especially in applications that use internal forwards (e.g., to an error page). This can cause redundant database calls or incorrect security context state.
  5. Stateful Filters: Filters in Spring are singletons by default. They should be completely stateless. Do not store any request-specific or user-specific data as instance variables in your filter class. All state should be managed within the scope of the `doFilterInternal` method.

Beyond Authentication: Other Use Cases for Custom Filters

While authentication is a primary use case, custom filters are versatile tools for a wide range of cross-cutting concerns.

  • Request Auditing Filter: A filter placed very early in the chain can log details of every request—IP address, headers, user agent—before any processing occurs, creating a comprehensive audit log.
  • Tenant Identification Filter: In a multi-tenant application, a filter can inspect the request (e.g., the hostname `tenant-a.myapp.com`) to identify the current tenant and set it in a `ThreadLocal` for the rest of the application to access.
  • Rate Limiting Filter: You can implement a filter to track the number of requests from a specific IP address or API key within a time window, returning an HTTP 429 (Too Many Requests) response if a limit is exceeded.

Conclusion: Your Gateway to Advanced Security

Mastering custom filters transforms Spring Security from a set of conventions into a fully programmable security engine. You gain fine-grained control over the entire request lifecycle, enabling you to implement any security requirement your application demands. By understanding the filter chain, leveraging the power of `OncePerRequestFilter`, and carefully considering filter placement with methods like `addFilterBefore`, you can solve complex challenges like stateless JWT authentication, API key validation, and detailed request auditing with clean, maintainable, and highly effective code.

By stepping beyond the default configuration and building your own filters, you are not just configuring a framework; you are architecting a security solution perfectly tailored to your application’s unique needs.

Scroll to Top