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





Spring Security Custom Filters: The Complete Guide (Spring Boot 3) | www.codegigs.app





Spring Security Custom Filters: The Complete Guide (Spring Boot 3)

Spring Security’s defaults are great—until they aren’t. Eventually, you’ll need to do something weird.

Maybe you need to check a custom X-Tenant-ID header. Maybe you’re validating a legacy API key. Or maybe you just want to log every request body before authentication happens.

At www.codegigs.app, custom filters are the #1 way we solve edge-case requirements for our enterprise students. Here is exactly how to write one that doesn’t break your application.

The Golden Rule: Use `OncePerRequestFilter`

You could implement the raw Jakarta Filter interface. Please don’t.

If you use a raw filter, it might run multiple times per request (e.g., once for the request, once for the forward, once for the error dispatch). This leads to weird bugs where your auth logic runs twice.

Spring provides OncePerRequestFilter. It does exactly what it says on the tin. Use it.

Scenario: Validating a Custom API Key

Let’s build a filter that checks for a header named X-API-KEY. If it’s missing or invalid, we reject the request immediately.

java
// ApiKeyAuthFilter.java
// Spring Boot 3.2+
package app.codegigs.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {

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

        // 1. Check for the header
        String apiKey = request.getHeader("X-API-KEY");

        // 2. Validate it (In reality, check DB or Redis)
        if ("secret-key-123".equals(apiKey)) {
            // Valid! Continue the chain
            filterChain.doFilter(request, response);
        } else {
            // Invalid! Block the request
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Invalid API Key");
            // DO NOT call filterChain.doFilter() here
        }
    }
}

The Trap: Notice line 31? If the validation fails, we write the response and STOP. We do not call filterChain.doFilter(). If you call it, the request continues to the controller even though you wanted to block it.

Where Do I Put It? (Filter Ordering)

Writing the filter is easy. Putting it in the right spot is where people mess up.

Spring Security is just a big list of filters. If you put your custom filter after the authentication filter, your custom logic might never run (because the user got rejected by the standard auth first).

For API keys or JWTs, you usually want to run before the UsernamePasswordAuthenticationFilter.

Java
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final ApiKeyAuthFilter apiKeyAuthFilter;

    public SecurityConfig(ApiKeyAuthFilter apiKeyAuthFilter) {
        this.apiKeyAuthFilter = apiKeyAuthFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            // HERE IS THE MAGIC
            .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class);
            
        return http.build();
    }
}

Common positions:

  • addFilterBefore(..., UsernamePasswordAuthenticationFilter.class): Best for custom auth tokens (JWT, API Keys).
  • addFilterAfter(..., ExceptionTranslationFilter.class): Good for logging or auditing successful requests.
  • addFilterAt(...): Replaces a standard filter (advanced use only).

Validating Authenticated Users (The Context Holder)

If your filter successfully authenticates a user (like verifying a JWT), you need to tell Spring Security about it. You do this by updating the SecurityContextHolder.

If you don’t do this, the next filter in the chain will say “Who is this?” and throw a 403.


// Inside doFilterInternal...
if (isValid(token)) {
    // Create an Authentication object
    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
        userString, 
        null, 
        List.of(new SimpleGrantedAuthority("ROLE_USER"))
    );
    
    // TELL SPRING WE ARE LOGGED IN
    SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);

This is the core logic behind our JWT Authentication guide. Once the context is set, Spring assumes the user is logged in.

Pro Tip: Filters are singletons. Do not store user-specific data in class fields (like private String currentUser). It will be overwritten by concurrent requests. Use local variables inside doFilterInternal only.

Common Mistakes I See

  1. Forgetting to Authenticate: You check the token but forget to update SecurityContextHolder. Result: 403 Forbidden.
  2. Double Execution: You annotated your filter with @Component AND manually added it with addFilterBefore. In some Spring Boot versions, this causes the filter to register twice (once in the default chain, once in the security chain). If your logic runs twice, remove @Component and just instantiate it in the config class.
  3. Exceptions in Filters: If your filter throws an exception, the global @ControllerAdvice won’t catch it because the request hasn’t reached the DispatcherServlet yet. You need to handle errors inside the filter or use an AuthenticationEntryPoint.

Filters Are Just the Beginning

Once you master filters, you control the entire security stack.

We build a complete multi-tenant security system using custom filters in the full course.

Start the Spring Security Master Class

Summary

  • Extend OncePerRequestFilter, not Filter.
  • Use addFilterBefore to run your logic before standard authentication.
  • Update SecurityContextHolder if your filter performs authentication.

Ready to apply this to OAuth2? Check out our next guide on Spring Security OAuth2.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top