Tutorials Logic, IN info@tutorialslogic.com

Servlet Filters Filter Chain, Logging, Authentication

Filter Chain

A Jakarta Servlet filter can inspect or wrap a request and response before the target servlet runs and after it returns. Filters are appropriate for transport-wide concerns such as correlation IDs, authentication integration, compression, and response headers. They should not contain the application’s business workflow.

This lesson assumes the servlet request lifecycle. After it, you can map a filter to the intended requests, call FilterChain exactly when processing should continue, preserve response behavior with wrappers, and avoid leaking request-specific state between threads.

What are Servlet Filters?

A Servlet Filter is a Java class that intercepts HTTP requests and responses before they reach a Servlet (or after the Servlet processes them). Filters implement the jakarta.servlet.Filter interface and are configured to intercept specific URL patterns.

Filters are ideal for cross-cutting concerns that apply to multiple Servlets:

  • Authentication and authorization
  • Logging and auditing
  • Request/response compression
  • Character encoding
  • CORS headers
  • Rate limiting
  • Input validation/sanitization

Logging Filter

Logging Filter
package com.example.filters;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import java.io.*;

// Apply to all URLs
@WebFilter("/*")
public class LoggingFilter implements Filter {

    private FilterConfig filterConfig;

    // Called once when filter is initialized
    @Override
    public void init(FilterConfig config) throws ServletException {
        this.filterConfig = config;
        System.out.println("LoggingFilter initialized");
    }

    // Called for every request matching the URL pattern
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest  req  = (HttpServletRequest)  request;
        HttpServletResponse resp = (HttpServletResponse) response;

        long startTime = System.currentTimeMillis();
        String uri     = req.getRequestURI();
        String method  = req.getMethod();
        String ip      = req.getRemoteAddr();

        System.out.println("[REQUEST] " + method + " " + uri + " from " + ip);

        // Pass request to next filter or servlet
        chain.doFilter(request, response);

        // Post-processing (after servlet response)
        long duration = System.currentTimeMillis() - startTime;
        int status = resp.getStatus();
        System.out.println("[RESPONSE] " + method + " " + uri
                + " -> " + status + " (" + duration + "ms)");
    }

    // Called once when filter is destroyed
    @Override
    public void destroy() {
        System.out.println("LoggingFilter destroyed");
    }
}

Authentication Filter

Authentication Filter and Filter Ordering

Authentication Filter and Filter Ordering
// Protect /admin/* URLs
@WebFilter("/admin/*")
public class AuthFilter implements Filter {

    private static final String[] PUBLIC_PATHS = {"/login", "/register", "/public"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest  req  = (HttpServletRequest)  request;
        HttpServletResponse resp = (HttpServletResponse) response;

        String path = req.getRequestURI().substring(req.getContextPath().length());

        // Check if path is public
        for (String publicPath : PUBLIC_PATHS) {
            if (path.startsWith(publicPath)) {
                chain.doFilter(request, response); // Allow through
                return;
            }
        }

        // Check if user is logged in
        HttpSession session = req.getSession(false);
        boolean loggedIn = (session != null && session.getAttribute("username") != null);

        if (loggedIn) {
            chain.doFilter(request, response); // Allow through
        } else {
            // Redirect to login
            resp.sendRedirect(req.getContextPath() + "/login?redirect=" + path);
        }
    }

    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

Authentication Filter - XML Configuration

Authentication Filter - XML Configuration
<!-- Filter ordering is defined by order in web.xml -->
<!-- Filters execute in the order they are declared -->

<filter>
    <filter-name>LoggingFilter</filter-name>
    <filter-class>com.example.filters.LoggingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LoggingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
    <filter-name>AuthFilter</filter-name>
    <filter-class>com.example.filters.AuthFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AuthFilter</filter-name>
    <url-pattern>/admin/*</url-pattern>
</filter-mapping>

Chain Control

The container constructs the configured chain and calls each filter’s doFilter method. Calling chain.doFilter passes control to the next filter or target resource; code after that call runs while the response unwinds. A filter may stop the chain for a valid reason, such as rejecting invalid authentication, but it must then complete the response with an appropriate status and body.

Ordering changes behavior. A correlation ID should exist before logging uses it, authentication should run before authorization, and compression belongs after code has produced response content. Keep each filter focused so ordering remains visible instead of embedding several unrelated policies in one class.

Mapping and Dispatch

Map filters narrowly by URL pattern, servlet name, and dispatcher types. REQUEST handles normal client dispatches; FORWARD, INCLUDE, ERROR, and ASYNC represent different container flows. Enabling every dispatcher without a reason can run the same filter more than once for one logical request.

Filter instances are shared across concurrent requests. Do not store a request, user, mutable buffer, or per-request timer in instance fields. Use local variables or request attributes, and release ThreadLocal values in a finally block if an integration requires them.

Response Wrapping

A wrapper can observe or transform request and response behavior, but it must preserve the original contract. Capturing a response body requires handling character encoding, writer versus output stream rules, status, headers, and committed responses. Prefer container features for standard compression and caching when they already solve the need.

Test both allowed and rejected paths. Verify the target servlet runs once on the allowed path, does not run on rejection, and that exceptions still trigger cleanup. These assertions catch missing chain calls and accidental double invocation.

Before you move on

Chain Review

4 checks
  • Call chain.doFilter once only when processing should continue.
  • Map only the URL and dispatcher types the policy needs.
  • Keep per-request state out of filter instance fields.
  • Run cleanup in finally when downstream code can throw.

Filter Failures

  • Forgetting chain.doFilter on the success path.

    Call the chain once after checks pass; otherwise the target never runs.
  • Writing a response and then continuing the chain.

    Return after completing a rejected response.
  • Keeping user state in a filter field.

    Use method-local state or request attributes.

Try this next

Trace the Chain

0 of 2 completed

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.