08 Pipeline Filtering How to Extend Security With Spring Security Filters

08 Pipeline Filtering How to Extend Security with Spring Security Filters #

In Lecture 06, “Permission Management: How to Analyze the Authorization Principles of Spring Security?” we introduced the concept of filters when discussing the Spring Security authorization process. Today, we will focus on this topic and analyze the filter architecture in Spring Security in detail, further learning the systematic methods to implement custom filters.

Spring Security Filter Architecture #

Filters are a common mechanism that play an important role in processing web requests. It can be said that all web development frameworks on the market more or less use filters to handle requests, and Spring Security is no exception. The filter architecture in Spring Security is built based on Servlet, so let’s start with the filters in Servlet.

Servlet and Pipeline-Filter Pattern #

Like most web request processing frameworks in the industry, the most basic architecture used in Servlet is the pipeline-filter pattern. The schematic diagram of the pipeline-filter pattern is as follows:

Drawing 0.png

Schematic diagram of the pipeline-filter pattern

Combining the above diagram, we can see that the component that handles business logic is called a filter, and the processing results are transmitted through pipes between neighboring filters, thus forming a filter chain.

In Servlet, the Filter interface representing filters is defined as follows:

public interface Filter {

    public void init(FilterConfig filterConfig) throws ServletException;

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    public void destroy();

}

When the application starts, the Servlet container will call the init() method. This method is only called once when the container starts, so it contains the relevant code for initializing the filter. Correspondingly, the destroy() method is used to release the resources occupied by the filter.

The business logic contained in a filter component should be in the doFilter() method, which takes three parameters: ServletRequest, ServletResponse, and FilterChain. These three parameters are all important, and we will explain them one by one.

  • ServletRequest: Represents the HTTP request. We use this object to obtain detailed information about the request.
  • ServletResponse: Represents the HTTP response. We use this object to build the response result and send it back to the client or pass it on along the filter chain.
  • FilterChain: Represents the filter chain. We use this object to forward the request to the next filter in the chain.

Please note that the filters in the filter chain are ordered, which is very important. We will explain this in detail in the subsequent content of this lecture.

Filter Chain in Spring Security #

In Spring Security, the execution of its core process also relies on a set of filters, which will be automatically initialized after the framework is started, as shown in the following diagram:

Drawing 1.png

Filter chain in Spring Security diagram

In the above diagram, we can see several common filters, such as BasicAuthenticationFilter, UsernamePasswordAuthenticationFilter, etc. These classes directly or indirectly implement the Filter interface in Servlet, and complete a certain specific authentication mechanism. For example, the BasicAuthenticationFilter in the diagram is used to verify the user’s identity credentials, while the UsernamePasswordAuthenticationFilter checks the entered username and password and decides whether to pass this result to the next filter based on the authentication result.

Please note that the end of the entire Spring Security filter chain is a FilterSecurityInterceptor, which is essentially a filter. However, unlike other filters used to complete authentication operations, its core function is to implement access control, that is, to determine whether the request can access the target HTTP endpoint. The granularity of access control by FilterSecurityInterceptor can be at the method level, which can meet the fine-grained access control mentioned before. We have already introduced this interceptor in detail in Lecture 06, “Permission Management: How to Analyze the Authorization Principles of Spring Security?”, so we will not expand on it here.

Through the above analysis, we have clarified that in Spring Security, the two security requirements of authentication and authorization are implemented through a series of filters. The true value of filters is not only in implementing authentication and authorization, but also providing an effective means for developers to extend the Spring Security framework.

Implementing Custom Filters #

Creating a new filter in Spring Security is not complicated; you just need to follow the protocol defined by the Servlet Filter interface.

Developing Filters #

Speaking of developing custom filters, the most classic application scenario is recording the access log of HTTP requests. Here is a common implementation:

public class LoggingFilter implements Filter {
    
    ...
}

Note: The remaining content of this section is omitted, as it contains technical details and code examples that cannot be properly translated.

private final Logger logger =
        Logger.getLogger(AuthenticationLoggingFilter.class.getName());

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

    HttpServletRequest httpRequest = (HttpServletRequest) request;

    // Retrieve the request data from the ServletRequest and log it
    String uniqueRequestId = httpRequest.getHeader("UniqueRequestId");
    logger.info("Successfully authenticated request: " +  uniqueRequestId);

    // Continue passing the request along the filter chain
    filterChain.doFilter(request, response);

}
}

Here we have defined a LoggingFilter to log a specific header called “UniqueRequestId” in requests that have been authenticated by the user. This unique request ID allows us to trace, monitor, and analyze the requests. When implementing a custom filter component, we typically retrieve request data from the ServletRequest and set response data in the ServletResponse, then continue passing the request along the filter chain using the doFilter() method of the FilterChain.

Next, let’s imagine a scenario where we need to determine the validity of a request based on whether a specific flag is present in the client’s request header. The diagram below illustrates this:

Drawing 2.png

Filter Design Diagram Based on Flag

This is a common use case in real-world development and allows for custom security controls. To address this use case, we can implement the RequestValidationFilter as shown below:

public class RequestValidationFilter implements Filter {

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

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    String requestId = httpRequest.getHeader("SecurityFlag");

    if (requestId == null || requestId.isBlank()) {
        httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return;
    }

    filterChain.doFilter(request, response);
}

}

Here, we retrieve the “SecurityFlag” from the request header of the HttpServletRequest object; otherwise, we directly throw a 400 Bad Request response. We can also implement various custom exception handling logic as needed.

Configuring Filters #

Now that we have implemented several valuable filters, the next step is to integrate these filters into the entire filter chain of Spring Security. Here, I would like to emphasize that, just like filters in Servlet, filters in Spring Security also have a specific order. This means that the placement of filters in the filter chain should comply with the functional characteristics of each filter itself, and these filters cannot be arbitrarily combined.

Let’s illustrate the importance of setting the filter order correctly with an example. In the lecture [“User Authentication: How to Build a User Authentication System Using Spring Security?”] we mentioned the HTTP basic authentication mechanism, which is implemented by BasicAuthenticationFilter in Spring Security.

If we want to implement a customized security control strategy, we can implement a filter like RequestValidationFilter mentioned earlier and place it before BasicAuthenticationFilter. This way, we can exclude a batch of invalid requests before performing user authentication, as shown below:

Drawing 3.png

Diagram of the position of RequestValidationFilter

In the above diagram, RequestValidationFilter ensures that requests without valid request header information do not need to go through unnecessary user authentication. Based on this scenario, it is not suitable to place RequestValidationFilter after BasicAuthenticationFilter because the user has already completed the authentication process.

Similarly, for the LoggingFilter we have built earlier, in principle, we can place it in any position in the filter chain because it only logs information. But is there a more appropriate position? Taking into account RequestValidationFilter, for an invalid request, logging is not meaningful. Therefore, LoggingFilter should be placed after RequestValidationFilter. On the other hand, for logging operations, it is usually only necessary to log requests that have been successfully authenticated, so it is also recommended to place LoggingFilter after BasicAuthenticationFilter. Finally, the relationship between these three filters is shown in the following diagram:

Drawing 4.png

Diagram of the positions of the three filters

In Spring Security, there is a set of utility methods to add filters to the filter chain, including addFilterBefore(), addFilterAfter(), addFilterAt(), and addFilter(), and they are all defined in the HttpSecurity class. The meaning of these methods is clear, and they are easy to use. For example, to achieve the effect shown in the above diagram, we can write the following code:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(
            new RequestValidationFilter(),
            BasicAuthenticationFilter.class)
        .addFilterAfter(
            new LoggingFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest()
            .permitAll();
}

Here, we use the addFilterBefore() and addFilterAfter() methods to add RequestValidationFilter and LoggingFilter before and after BasicAuthenticationFilter, respectively.

Filters in Spring Security #

The following table lists the commonly used filter names, their functions, and their order relationships in Spring Security:

image.png

List of commonly used filters in Spring Security

Taking the most basic filter UsernamePasswordAuthenticationFilter as an example, its definition and core method, attemptAuthentication, are as follows:

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {

    public Authentication attemptAuthentication(HttpServletRequest request,
           HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
           throw new AuthenticationServiceException(
                   "Authentication method not supported: " + request.getMethod());
        }
    
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
           username = "";
        }

        if (password == null) {
           password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
               username, password);

        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
}

Based on the above method and the previously introduced authentication and authorization implementation principles, we can introduce a series of core classes in the framework and outline their interaction structures, as shown in the following diagram:

image

Core classes related to UsernamePasswordAuthenticationFilter

Many of the classes in the above diagram can be understood by their names. Taking SecurityContextHolder located in the lower-left corner as an example, it is a typical Holder class that stores the security context object SecurityContext of the application, which contains the user’s authentication information.

We can also boldly speculate that it should use ThreadLocal internally to ensure thread-safe access. More specifically, we have explained the usage of SecurityContext in the lecture [“Permissions Management: How to Analyze the Authorization Principles of Spring Security?”].

As demonstrated by the code in UsernamePasswordAuthenticationFilter, after an HTTP request arrives, user authentication is completed through a series of filters, and the specific work is delegated to AuthenticationManager, which involves the interaction between multiple core components such as AuthenticationProvider and UserDetailsService. For a detailed description of the authentication process in Spring Security, you can refer to the lecture [“Authentication System: How to Deeply Understand the User Authentication Mechanism of Spring Security?”] for a review.

Summary and Preview #

In this lecture, we focused on a core component of Spring Security - filters. In a request-response processing framework, filters play an important role by intercepting requests and defining authentication and authorization logic. At the same time, we can implement various custom filter components according to needs and dynamically expand Spring Security. This lecture provided a detailed introduction to the filter architecture and development methods in Spring Security, which you can study repeatedly.

The summary of this lecture is as follows:

Drawing 6.png

Finally, I leave you with a question to ponder: In Spring Security, can you briefly describe the process of performing user authentication using filters? Feel free to share your thoughts in the comments section.