Skip to content
Ashish.
All posts
Diagram comparing GenericFilterBean and OncePerRequestFilter execution flows in async contexts.
6 min readDevelopmentJava DevelopersFeatured#spring security#jwt#java#web security#filter chain#onceperrequestfilter#backend development

OncePerRequestFilter: The Right Base Class for JWT

Learn why OncePerRequestFilter is the correct base class for JWT authentication in Spring Security, handling async dispatches and avoiding duplicate processing.

By Ashish KumarPart 4 of Spring Security Filter Chain Mastery

When implementing JWT authentication in Spring Security, developers often reach for GenericFilterBean as the simplest path to creating a filter. It works for simple header checks in synchronous requests. However, in production environments involving asynchronous processing, GenericFilterBean introduces subtle security vulnerabilities and performance degradation. The correct base class for any JWT filter is OncePerRequestFilter. This distinction is not merely stylistic; it is a mechanism-level requirement for maintaining security integrity across the full lifecycle of an HTTP request, including async dispatches.

The Failure Mode of GenericFilterBean

Understanding these Java security pitfalls is essential for robust filter design. To understand why OncePerRequestFilter is necessary, we must first understand how Spring’s FilterChain operates. A Filter implements doFilter(ServletRequest request, ServletResponse response, FilterChain chain). When you extend GenericFilterBean, you override this method to parse the JWT from the Authorization header.

The problem arises when the FilterChain is invoked more than once for the same logical HTTP request. This happens during asynchronous processing. Consider a controller method that returns a Callable<String>. The main thread executes the filter, validates the JWT, sets the SecurityContext, and then hands off the execution to an async thread. The main thread completes the HTTP response handling but does not finish the logical request processing. Later, the async thread resumes execution, and the DispatcherServlet re-invokes the filter chain to complete the response.

If your JWT filter extends GenericFilterBean, it has no memory of the previous invocation. It receives the request again, attempts to parse the Authorization header, and validates the token a second time. This leads to two critical issues:

  1. Redundant Processing: You perform cryptographic verification twice, wasting CPU cycles.
  2. Stream and Attribute Errors: If your filter consumes the input stream (e.g., to read a body) or modifies request attributes, the second invocation may fail with an IllegalStateException because the stream is already closed or the attributes are inconsistent. This is distinct from logical state corruption of the security context, which remains valid but is redundantly processed.

Even if you attempt to deduplicate by checking for a custom header like X-JWT-Processed, this approach is fragile. Async dispatches may use different HttpServletRequest wrappers, and header checking alone does not guarantee that the security context was properly propagated or that the filter’s side effects were idempotent.

Mechanism of OncePerRequestFilter

This approach is a standard pattern in Java security frameworks. OncePerRequestFilter solves this by maintaining state as a request attribute. It uses a constant suffix, ALREADY_FILTERED_SUFFIX (".FILTERED"), which is appended to the filter's name (or class name) to build a unique attribute key for that filter.

The mechanism works as follows:

  1. Entry Check: Before executing the filter logic, OncePerRequestFilter checks request.getAttribute(alreadyFilteredAttributeName).
  2. Short-Circuit: If the attribute exists, it skips the doFilterInternal method and immediately calls chain.doFilter(request, response).
  3. Marking: If the attribute does not exist, it sets the attribute to Boolean.TRUE before calling doFilterInternal.

This ensures that regardless of how many times the FilterChain is invoked for the same request object, the heavy lifting of JWT parsing and validation occurs exactly once.

public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        
        String jwt = extractJwt(request);
        
        if (jwt != null && jwtValidator.validate(jwt)) {
            Authentication auth = tokenToAuth(jwt);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        // Proceed with the chain. 
        // If this is a second async call, OncePerRequestFilter's doFilter() would have skipped this method entirely.
        filterChain.doFilter(request, response);
    }
}

Async Dispatch Scenario: A Worked Example

This scenario illustrates a critical aspect of Java security in async environments. Let’s trace a specific scenario to see the data flow.

Actors:

  • Client: Sends a POST request with a valid JWT.
  • JwtFilter: Extends OncePerRequestFilter.
  • DispatcherServlet: Manages the request lifecycle.
  • AsyncThread: Processes the business logic.

Step 1: Initial Request The client sends a request. The DispatcherServlet invokes the filter chain.

  • JwtFilter.doFilterInternal is called.
  • FILTERED attribute is absent.
  • FILTERED is set to TRUE.
  • JWT is parsed and validated. SecurityContext is populated.
  • filterChain.doFilter is called, passing control to the controller.

Step 2: Async Handoff The controller returns a Callable. The main thread completes the filter chain processing. The DispatcherServlet registers an AsyncListener and returns the HTTP response status (e.g., 200 OK) to the client, but the logical request is still open.

Step 3: Async Resume The AsyncThread begins execution. The DispatcherServlet re-invokes the filter chain to allow filters to perform post-processing or cleanup.

  • JwtFilter.doFilter is invoked again.
  • It finds the already-filtered attribute set to TRUE.
  • OncePerRequestFilter skips the doFilterInternal method, bypassing the JWT parsing logic.
  • It directly calls chain.doFilter(request, response).

Without OncePerRequestFilter, Step 3 would re-parse the JWT. If the JWT was short-lived or if the filter had side effects (like consuming the request body), the second invocation could fail or produce inconsistent security states. By using OncePerRequestFilter, the SecurityContext established in Step 1 remains intact and is not overwritten or re-validated unnecessarily.

Why GenericFilterBean Fails Under Pressure

You might argue that you can manually implement this check in GenericFilterBean:

// Fragile approach
if (request.getAttribute("jwt_processed") != null) {
    chain.doFilter(request, response);
    return;
}
request.setAttribute("jwt_processed", Boolean.TRUE);

This looks similar, but it lacks the robustness of OncePerRequestFilter. The OncePerRequestFilter implementation handles edge cases in the Servlet API, such as different HttpServletRequest implementations (e.g., in portlets or certain application servers) and ensures the attribute is scoped correctly to the request lifecycle. Furthermore, it integrates with Spring’s DispatcherType handling. If you configure your security to exclude certain dispatch types, OncePerRequestFilter respects these configurations more predictably than a manual attribute check.

Best Practices for JWT Filters

Adhering to these Java security standards prevents common vulnerabilities. When implementing your JWT filter, adhere to these rules:

  1. Always Extend OncePerRequestFilter: This is non-negotiable for production-grade applications.
  2. Handle Exceptions Gracefully: If JWT validation fails, do not throw unchecked exceptions that crash the thread. Return a 401 Unauthorized or 403 Forbidden response and skip the rest of the chain.
  3. Do Not Consume the Request Body: JWTs are typically in headers. If you need to read the body, ensure you use a ContentCachingRequestWrapper or similar, and be aware that this wrapper can only be read once. OncePerRequestFilter helps ensure you don’t accidentally try to read it twice.
  4. Order Matters: Place your JWT filter early in the chain, but after any filters that might modify the request (like logging or compression). Use @Order or FilterRegistrationBean to control sequence.

Conclusion

The choice between GenericFilterBean and OncePerRequestFilter is a choice between simplicity and correctness. For JWT authentication, where state consistency and security are paramount, OncePerRequestFilter provides the necessary mechanism to handle the complexities of modern web servers, including asynchronous processing. By leveraging its built-in deduplication logic, you ensure that your authentication logic runs exactly once per logical request, protecting your application from redundant processing and potential security lapses. By following these guidelines, you uphold strong Java security standards in your Spring applications.

Related posts