Image of How to exclude a URL from a Servlet Filter

ADVERTISEMENT

Introduction

By default, servlet filters don’t support excluding a specific URL pattern, whenever you define a URL pattern for a filter then any request matching this pattern is processed by the filter without exceptions.

In this tutorial, we show how to programmatically add an exclude functionality to an existing servlet filter.

1- Adding exclude functionality to a custom Filter

Suppose we have an existing web application that authenticates user requests through LDAP. All the servlet requests pass through LDAPAuthenticationFilter which is mapped to /* as the following:

<filter>
    <filter-name>LDAPAuthenticationFilter</filter-name>
    <filter-class>com.programmer.gate.filters.LDAPAuthenticationFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LDAPAuthenticationFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Our filter simply authenticates the request and calls chain.doFilter() afterwards:

package com.programmer.gate.filters;
 
import java.io.IOException;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
 
public class LDAPAuthenticationFilter implements Filter{
 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
 
        // Authenticate the request through LDAP
        System.out.println("Authenticating the request through LDAP");
        
        // Forward the request to the next filter or servlet in the chain.
        chain.doFilter(req, resp);
    }
    
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    
    public void destroy() {
        // TODO Auto-generated method stub
    }
}

Now, suppose we want to create a servlet which requires a simple database authentication and needs not to pass through LDAP. The first thing we think of is to create a new filter and map it to the specific URL pattern of the new servlet.

So we create a new filter named as DatabaseAuthenticationFilter which simply authenticates the request through the database and calls chain.doFilter() afterwards:

package com.programmer.gate.filters;
 
import java.io.IOException;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
 
public class DatabaseAuthenticationFilter implements Filter{
 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
 
        // Authenticate the request through database then forward the request to the next filter or servlet in the chain
        System.out.println("Authenticating the request through database");
        
        chain.doFilter(req, resp);
    }
    
    public void init(FilterConfig arg0) throws ServletException {
        // TODO Auto-generated method stub
        
    }
    
    public void destroy() {
        // TODO Auto-generated method stub
        
    }
}

We define our filter under web.xml to handle only specific URLs starting with /DatabaseAuthenticatedServlet:

<filter>
    <filter-name>DatabaseAuthenticationFilter</filter-name>
    <filter-class>com.programmer.gate.filters.DatabaseAuthenticationFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>DatabaseAuthenticationFilter</filter-name>
    <url-pattern>/DatabaseAuthenticatedServlet/*</url-pattern>
</filter-mapping>

The problem here is that requests like /DatabaseAuthenticatedServlet would also match the root URL pattern “/*”, i.e. our request would pass through 2 authentication processes: LDAP and Database, the ordering depends on which filter is defined first under web.xml.

In order to solve this, we need to modify LDAPAuthenticationFilter so that it excludes URLs starting with /DatabaseAuthenticatedServlet. What people normally do is statically check over the servlet URL of the request inside doFilter() method and simply bypass the authentication process when found.

Here we go a step further and implement a more dynamic solution that allows us to manage the excluded URLs through web.xml.

Following are the steps for adding the exclude feature to LDAPAuthenticationFilter:

Add a new field called excludedUrls of type List:

private List<String> excludedUrls;

Inside init() method, read a configuration attribute called excludedUrls using FilterConfig, the attribute is supposed to be comma-separated so that we exclude as much URLs as we need.

public void init(FilterConfig filterConfig) throws ServletException {
    String excludePattern = filterConfig.getInitParameter("excludedUrls");
    excludedUrls = Arrays.asList(excludePattern.split(","));
}

Modify doFilter() in order to check if the requested URL belongs to the list of predefined excluded URLs, if so then just forward the request to the next filter or servlet in the chain, otherwise do your authentication logic.

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
 
    String path = ((HttpServletRequest) req).getServletPath();
        
    if(!excludedUrls.contains(path))
    {
        // Authenticate the request through LDAP
        System.out.println("Authenticating the request through LDAP");
    }
        
    // Forward the request to the next filter or servlet in the chain.
    chain.doFilter(req, resp);
}

Now inside web.xml, you can control which URL to exclude from LDAP authentication without any single code change:

<filter>
    <filter-name>LDAPAuthenticationFilter</filter-name>
    <filter-class>com.programmer.gate.filters.LDAPAuthenticationFilter</filter-class>
    <init-param>
        <param-name>excludedUrls</param-name>
        <!-- Comma separated list of excluded servlets  -->
        <param-value>/DatabaseAuthenticatedServlet,/UnAuthenticatedServlet</param-value>
    </init-param>
</filter>

This is how LDAPAuthenticationFilter looks like after adding the exclude functionality:

package com.programmer.gate.filters;
 
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
 
public class LDAPAuthenticationFilter implements Filter{
    
    private List<String> excludedUrls;
 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
 
        String path = ((HttpServletRequest) req).getServletPath();
        
        if(!excludedUrls.contains(path))
        {
            // Authenticate the request through LDAP
            System.out.println("Authenticating the request through LDAP");
        }
        
        // Forward the request to the next filter or servlet in the chain.
        chain.doFilter(req, resp);
    }
    
    public void init(FilterConfig filterConfig) throws ServletException {
        String excludePattern = filterConfig.getInitParameter("excludedUrls");
        excludedUrls = Arrays.asList(excludePattern.split(","));
    }
    
    public void destroy() {
        // TODO Auto-generated method stub
    }
}

2- Adding exclude functionality to a third-party Filter

The third-party filters are the filters that you can’t control. i.e. you can’t modify their source code.

In this section, we alter our example a bit and use CAS authentication instead of LDAP. This is how we define our CAS authentication filter in web.xml:

<filter>
  <filter-name>CAS Authentication Filter</filter-name>
  <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
  <init-param>
    <param-name>casServerLoginUrl</param-name>
    <param-value>https://localhost:8443/cas/login</param-value>
  </init-param>
  <init-param>
    <param-name>serverName</param-name>
    <param-value>localhost</param-value>
  </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Authentication Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

CAS authentication is done through a third-party library, now in order to support database authentication we can’t modify the source code of CAS as we did in the previous example with LDAP.

The solution for excluding URLs from a third-party filter is to wrap it with a new custom filter which just adds the exclude functionality and delegates the filter logic to the wrapped class.

Following are the steps for adding exclude functionality to CAS authentication:

Create a new filter called CASCustomAuthenticationFilter as the following:

public class CASCustomAuthenticationFilter implements Filter{
    
    private AuthenticationFilter casAuthenticationFilter = new AuthenticationFilter();
    private List<String> excludedUrls;
 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
 
        String path = ((HttpServletRequest) req).getServletPath();
        
        if(!excludedUrls.contains(path))
        {
            // Authenticate the request through CAS
            casAuthenticationFilter.doFilter(req,resp,chain);
        }
        
        // Forward the request to the next filter or servlet in the chain.
        chain.doFilter(req, resp);
    }
    
    public void init(FilterConfig arg0) throws ServletException {
 
        String excludePattern = filterConfig.getInitParameter("excludedUrls");
        excludedUrls = Arrays.asList(excludePattern.split(","));
        
        casAuthenticationFilter.init();
    }
    
    public void destroy() {
        casAuthenticationFilter.destroy();
    }
}

Our custom filter wraps the CAS authentication filter through composition, its main purpose is to just manage which URLs to be authenticated through CAS , while we didn’t touch the CAS authentication procedure.

In web.xml, we change the filter definition to use CASCustomAuthenticationFilter instead of the default CAS implementation:

<filter>
  <filter-name>CAS Authentication Filter</filter-name>
  <filter-class>com.programmer.gate.filters.CASCustomAuthenticationFilter</filter-class>
  <init-param>
    <param-name>casServerLoginUrl</param-name>
    <param-value>https:localhost:8443/cas/login</param-value>
  </init-param>
  <init-param>
    <param-name>serverName</param-name>
    <param-value>localhost</param-value>
  </init-param>
  <init-param>
    <param-name>excludeUrls</param-name>
    <param-value>/DatabaseAuthenticatedServlet</param-value>
  </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Authentication Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

That’s it, please leave your thoughts in the comments section below.

Summary

By default, servlet filters don’t support excluding a specific URL pattern, whenever you define a URL pattern for a filter then any request matching this pattern is processed by the filter without exceptions.

Next Steps

If you're interested in learning more about the basics of Java, coding, and software development, check out our Coding Essentials Guidebook for Developers, where we cover the essential languages, concepts, and tools that you'll need to become a professional developer.

Thanks and happy coding! We hope you enjoyed this article. If you have any questions or comments, feel free to reach out to jacob@initialcommit.io.