SpringBoot HandlerInterceptor actual combat

HandlerInterceptor

  • 1. Introduction to interceptors
    • 1.1 Interface method description
      • 1.1.1 preHandle
      • 1.1.2 postHandle
      • 1.1.3 afterCompletion
    • 1.2 Basic usage examples
      • 1.2.1 Implement custom interceptor
      • 1.2.2 Register interceptor
  • 2. Actual combat
    • 2.1 Practical combat one: Obtain controller method annotation authentication through interceptor
      • 2.1.1 Define permission annotations
      • 2.1.2 Implement permission interceptor (implement HandlerInterceptor interface)
      • 2.1.3 Configuring interceptors
      • 2.1.4 Controller usage permission annotations
    • 2.2 Practical Combat 2: Implement third-party HTTP request signature through interceptor
      • 2.2.1 Interface signature tool class
      • 2.2.2 Implement security interceptor (implement HandlerInterceptor interface)
      • 2.2.3 Register interceptor
      • 2.2.4 application.yml configuration
  • 3. Expand
    • 3.1 Execution order and differences between Interceptor and Filter
      • 3.1.1 Filter
      • 3.1.2 Interceptor
      • 3.1.3 Summary
  • 4. Reference articles

1. Introduction to interceptors

HandlerInterceptorAdapter is the interceptor class in Spring MVC, which is used to intercept the request processing flow, including request preprocessing, postprocessing, and rendering views. It can be used to implement some global processing logic, such as logging, permission verification, request parameter preprocessing, etc.

1.1 Interface method description

1.1.1 preHandle

Preprocessing callback method implements preprocessing of the processor. Permission verification, request parameter processing, etc. can be performed here.

  • Return value: true means the process continues; false means the process is interrupted and other interceptors or processors will not be called.

1.1.2 postHandle

The post-processing callback method implements post-processing of the processor (controller), but before rendering the view, we can process the model data or process the view through modelAndView.

1.1.3 afterCompletion

The callback method after the entire request is processed, that is, the callback is called when the view is rendered. For example, in performance monitoring, we can record the end time and output the consumption time here, and we can also perform some resource cleanup, similar to finally in try-catch-finally, but Only calls within the processor execution chain

1.2 Basic usage examples

1.2.1 Implementing custom interceptors

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor extends HandlerInterceptorAdapter {<!-- -->

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {<!-- -->
        // Executed before request processing, permission verification and other operations can be performed
        System.out.println("Pre Handle method is Calling");
        return true; // Returning true means continuing to perform subsequent operations, returning false will interrupt the request processing process
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {<!-- -->
        // Executed after request processing and before view rendering, you can modify the data model and other operations
        System.out.println("Post Handle method is Calling");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {<!-- -->
        // Executed after the entire request is completed, it can be used for operations such as cleaning up resources.
        System.out.println("After Completion method is Calling");
    }
}

In the above example, MyInterceptor inherits the HandlerInterceptorAdapter class and overrides its three methods (preHandle, postHandle, and afterCompletion)

1.2.2 Register interceptor

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {<!-- -->

    @Override
    public void addInterceptors(InterceptorRegistry registry) {<!-- -->
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }
}

In the above configuration, the MyInterceptor interceptor is registered in the addInterceptors method, and the interception path is set to “/**”, which means that all requests are intercepted. You can modify the intercepted path according to actual needs

2. Practical combat

2.1 Practical Combat 1: Obtain controller method annotation authentication through interceptor

2.1.1 Define permission annotations

@Retention(RUNTIME)
@Target(METHOD)
public @interface ApiPermission {
    String value() default "";//The default is empty, because the name is value, you don't need to write "value=" in actual operation
}

2.1.2 Implement permission interceptor (implement HandlerInterceptor interface)

@Component
public class PermissionInterceptor implements HandlerInterceptor {<!-- -->
    private static final Logger LOGGER = LoggerFactory.getLogger(PermissionInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {<!-- -->
        LOGGER.info("Enter interceptor");
        if(handler instanceof HandlerMethod) {<!-- -->
            HandlerMethod h = (HandlerMethod)handler;
         ApiPermission apiPermission= h.getMethodAnnotation(ApiPermission.class);
        if (easyLog != null) {<!-- -->
        //TODO determines whether the current user has permission information
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            //Create a JSON object
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("code", "403");
            jsonObject.put("message", "User does not have permission to operate");

            //Set the response content type to JSON and use UTF-8 encoding (if you don’t use Chinese, it will be garbled)
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().print(jsonObject.toString());
            return false;
        }
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

}

Get the controller class method annotation: h.getMethod().getDeclaringClass().getAnnotation(ApiPermission.class);

2.1.3 Configuring interceptors

@Component
public class WebMvcConfig implements WebMvcConfigurer {<!-- -->
    @Autowired
    private PermissionInterceptor permissionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {<!-- -->
        registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");;
    }
}

2.1.4 Controller permission annotations

@Controller
@RequestMapping("/api/user")
public class UserController {<!-- -->

@Autowired
private UserService userService;

@ApiPermission("user:list")
@RequestMapping(value = "/list", method=RequestMethod.GET)
@ResponseBody
public ApiResult<PageResult<Page<User>>> listByPage(User user, PageResult pageResult) {<!-- -->
Page<User> page = this.userService.findListByPage(user, new Page(pageResult.getPageNum(), pageResult.getPageSize()));
return ApiResult.wrapPage(page);
}
}

2.2 Practical Combat 2: Implementing third-party HTTP request signature through interceptor

Signature is a security verification method commonly used in web development to ensure the origin and integrity of requests.

2.2.1 Interface signature tool class

The steps inside the method are as follows:

  1. Construct the string to be signed toSign: Concatenate the HTTP request method, URI and timestamp into a string.
  2. Initialize the MAC algorithm: Use the HmacSHA256 algorithm (a message authentication code algorithm based on a hash function) for signature. Here, use the Mac class to initialize the HmacSHA256 algorithm, passing in the SecretKeySpec object as the key.
  3. Calculate the signature: Convert the string toSign to be signed into a byte array using UTF-8 encoding, and then calculate it through the HMAC-SHA1 algorithm to generate a signed byte array.
  4. Base64 encoding: Base64 encode the generated signature byte array to obtain the final signature string.
  5. Return signature result: Return the generated signature string.

Ultimately, the method returns a signature string generated based on the given HTTP request method, URI, timestamp, and key. This signature string can be used to verify the legitimacy of the request on the server side, ensure the source and integrity of the request, and prevent malicious requests and data tampering.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * Service authentication signature tool class
 */
public class ServiceSignUtil {<!-- -->

    public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    // Define other algorithm constants...

    private ServiceSignUtil() {<!-- -->
    }

    /**
     * Generate service authentication signature
     *
     * @param httpMethod HTTP request method, such as GET, POST, etc.
     * @param uri requested URI (path)
     * @param utc requested timestamp (usually a timestamp in UTC format)
     * @param secretKey key used for signing
     * @param algorithm signature algorithm, using the constants defined above
     * @return returns the generated signature string
     * @throws NoSuchAlgorithmException if the specified algorithm does not exist
     * @throws InvalidKeyException if the key is invalid
     */
    public static String signature(String httpMethod, String uri, String utc, String secretKey, String algorithm)
            throws NoSuchAlgorithmException, InvalidKeyException {<!-- -->
        String toSign = httpMethod + uri + utc;
        Mac mac = Mac.getInstance(algorithm);
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), algorithm));
        byte[] signatureBytes = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signatureBytes).trim();
    }
}

2.2.2 Implement security interceptor (implement HandlerInterceptor interface)

public class ApiSecurityInterceptor extends HandlerInterceptorAdapter {<!-- -->
    private static final Logger LOGGER = LoggerFactory.getLogger(ApiSecurityInterceptor.class);

    private String accessKey = "ak";
    private String secretKey = "sk";

 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {<!-- -->
         
        String accessId = request.getHeader(RemoteCallConstants.HEADER_ACCESS_ID);
        String signature = request.getHeader(RemoteCallConstants.HEADER_SIGNATURE);
        String utc = request.getHeader(RemoteCallConstants.HEADER_TIMESTAMP);
        // The microservice identifier is not used for the time being.
        //String appService = request.getHeader(RemoteCallConstants.HEADER_APP_SERVICE);
        // If the authentication information is empty, it is considered an illegal request.
        if (StringUtils.isBlank(accessId) || StringUtils.isBlank(signature) || StringUtils.isBlank(utc)) {<!-- -->
            throw new RuntimeException("MessageEnum.ILLEGAL_REQUEST");
        }
 
        DateTime dateTime = DateUtil.parseUTC(utc);
        //The request is valid within 5 minutes. If it exceeds, the request will be considered expired.
        if (DateTime.now().between(dateTime).between(DateUnit.MINUTE) >= 5L) {<!-- -->
            throw new RuntimeException("MessageEnum.REQUEST_HAS_EXPIRED");
        }
 
        // If the access-key does not match, the authentication fails.
        if (!StringUtils.equals(accessKey, accessId)) {<!-- -->
            throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
        }
 
        String uri = request.getServletPath();
        String httpMethod = request.getMethod();
 
        try {<!-- -->
            String currentSignature = ServiceSignUtil.signature(httpMethod, uri, utc, secretKey, "HmacSHA256");
            //The signatures are inconsistent
            if (!StringUtils.equals(currentSignature, signature)) {<!-- -->
                throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
            }
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {<!-- -->
            // Signature generation failed
            throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
        }
 
        return true;
    }

    public String getAccessKey() {<!-- -->
        return accessKey;
    }

    public void setAccessKey(String accessKey) {<!-- -->
        this.accessKey = accessKey;
    }

    public String getSecretKey() {<!-- -->
        return secretKey;
    }

    public void setSecretKey(String secretKey) {<!-- -->
        this.secretKey = secretKey;
    }
}
  • constant class
public class RemoteCallConstants {<!-- -->
 
    public static final String HEADER_ACCESS_ID = "accessId";
 
    public static final String HEADER_TIMESTAMP = "timestamp";
 
    public static final String HEADER_SIGNATURE = "signature";
 
    public static final String QUERY_LANG = "lang";
 
    public static final String UTC_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
 
    private RemoteCallConstants() {<!-- -->
    }
}

2.2.3 Register interceptor

@Configuration
public class InterceptorConfig {<!-- -->
    private static final Logger LOGGER = LoggerFactory.getLogger(InterceptorConfig.class);

    @Value("${easy-api.security.path-patterns:/**/api/**}")
    private String pathPatterns;
    /**
     * The second usage of @ConfigurationProperties @Bean + @ConfigurationProperties
     * Notice! The properties in the corresponding bean require setter/getter methods, otherwise they cannot be injected.
     */
    @Bean
    @ConfigurationProperties(prefix = "easy-api.security")
    public ApiSecurityInterceptor apiSecurityInterceptor() {<!-- -->
        return new ApiSecurityInterceptor();
    }

    @Bean
    public WebMvcConfigurer apiSecurityInterceptorConfigurer() {<!-- -->
        LOGGER.info("[easy-mode]@EnableEasyApiSecurity[apiSecurityInterceptor]--Intercept:{}", pathPatterns);
        return new WebMvcConfigurer() {<!-- -->
            @Override
            public void addInterceptors(InterceptorRegistry registry) {<!-- -->
                registry.addInterceptor(apiSecurityInterceptor()).addPathPatterns(pathPatterns);
            }
        };
    }
}

2.2.4 application.yml configuration

easy-api:
  security:
    access-key: ak
    secret-key:sk
    path-patterns: /**

The access interface ak sk and the authentication interface are flexibly defined through configuration files. (The demonstration here is hard-coded for convenience. In the production environment, one pair is usually used for each caller)

3. Expansion

3.1 The execution order and difference between Interceptor and Filter

3.1.1 Filter

  • Container: It relies on the Servlet container and is part of the Servlet specification, while the interceptor exists independently and can be used in any situation.
  • Technology: In terms of implementation, it is based on function callbacks.
  • Advantages and Disadvantages: It can filter almost all requests, but the disadvantage is that a filter instance can only be init once when the container is initialized.
  • The purpose of using filters: is to do some filtering operations to obtain the data we want to obtain. For example: in JavaWeb, filter out some information in advance for the incoming request and response, or set some parameters in advance and then pass them Enter servlet or Controller to perform business logic operations.
  • Commonly used scenarios are:
    • Filtering: Modify the character encoding (CharacterEncodingFilter) in the filter, modify some parameters of HttpServletRequest (XSSFilter (custom filter)) in the filter, such as filtering vulgar text, dangerous characters, etc.
    • Identity authentication: If the resources provided by the server are protected, session authentication is implemented in the filter, such as shiro (authority framework)
    • Request forwarding: Change the specific request path, such as SSO’s cas client filter, 302 forward cas login page, and use the ticket parameter to initiate verification and obtain user information after logging in.
    • Logging: Combined with log4j’s MDC to record requested IP/user and other information, create requestId for request link tracking.

3.1.2 Interceptor

  • Container: depends on the web framework, in SpringMVC it depends on the SpringMVC framework
  • Technology: The implementation is based on Java’s reflection mechanism, which is an application of aspect-oriented programming (AOP).
  • Advantages and Disadvantages: Since the interceptor is based on the call of the web framework, Spring’s dependency injection (DI) can be used to perform some business operations. At the same time, an interceptor instance can be called multiple times within a controller life cycle. Disadvantages: Depends on web framework
  • Purpose of using interceptors: Generally, filters will be configured before executing the web framework. The control scope is relatively large. Basic filters can meet most needs. Interceptors are generally used where the filter implementation is not friendly enough. For example, getting container context information
  • Commonly used scenarios are:
    • Permission authentication: The annotations of the controller class or method can be easily obtained through the HandlerMethod of the interceptor, which is very convenient for permission control of the interface.

3.1.3 Summary

Interceptor combines containers to be closer to the business, and filters tend to be more basic and universal configurations.

4. Reference articles

  1. Obtain the Controller method name and annotation information from the SpringMVC interceptor (used to verify permissions)
  2. The execution order and difference between Interceptor and Filter
  3. 6 differences between filters and interceptors, don’t be confused anymore