Common filters and interceptors written in springboot

The difference between filters and interceptors

In Spring Boot, filters (Filter) and interceptors (Interceptor) are important parts of processing HTTP requests. Although they are somewhat similar in function, their usage scenarios and implementation methods are different. Here are the main differences between the two:

  1. At the level : The filter is part of the Servlet specification and is managed by the Servlet container. Interceptors are part of Spring MVC and are managed by DispatcherServlet. This means filters are executed before interceptors.

  2. Intercept scope : The filter can intercept all requests, including static resources, such as HTML, JS, CSS, etc. The interceptor only intercepts those requests processed by Spring's DispatcherServlet.

  3. Parameter passing : Filters can only use ServletRequest and ServletResponse objects. The interceptor can access more specific request and response objects (for example, HttpServletRequest and HttpServletResponse), and can also obtain information about the methods and classes that process requests through HandlerMethod.

  4. Function : Filters are mainly used to handle general request processing, such as: setting request encoding, filtering illegal characters, preventing SQL injection, etc. Interceptors are mainly used to process business logic, such as: login check, permission verification, program execution time, etc.

  5. Underlying implementation : The implementation of the filter is based on the callback function. The implementation of the interceptor (proxy mode) is based on reflection

Introduce dependencies

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.27</version>
        </dependency>
    </dependencies>

1. Solve the problem that the request body cannot be read repeatedly and write an interceptor to record the log

Customize the wrapper to copy the request stream. If you do not rewrite it, java.lang.IllegalStateException: getInputStream() has already been called for this request will be reported. The reason is that the request stream cannot be read repeatedly.

import lombok.Getter;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    /** 复制请求body */
    @Getter
    private final String bodyString;

    public MyHttpServletRequestWrapper (HttpServletRequest request) throws IOException {
        super(request);
        this.bodyString = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        this.body = this.bodyString.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        //返回body的流信息即可
        try(final ByteArrayInputStream bais = new ByteArrayInputStream(this.body)){
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {
                }
                @Override
                public int read() {
                    return bais.read();
                }
            };
        }
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

}

Custom filter, you must pass the custom wrapper through the filter, if you don't pass it, the rewritten getInputStream() and getReader() methods will not be called

import com.example.filter.demo.wrapper.MyHttpServletRequestWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

@Component
@WebFilter(filterName = "RewriteRequestFilter", urlPatterns = "/*")
@Order(0)
public class RewriteRequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //文件上传类型 不需要处理,否则会报java.nio.charset.MalformedInputException: Input length = 1异常
        if (Objects.isNull(request) || Optional.ofNullable(request.getContentType()).orElse(StringUtils.EMPTY).startsWith("multipart/")) {
            chain.doFilter(request, response);
            return;
        }
        //自定义wrapper 处理流,必须在过滤器中处理,然后通过FilterChain传下去, 否则重写后的getInputStream()方法不会被调用
        MyHttpServletRequestWrapper requestWrapper = new MyHttpServletRequestWrapper((HttpServletRequest)request);
        chain.doFilter(requestWrapper,response);
    }
}

 custom log class

import lombok.Data;

import java.util.Map;

@Data
public class LoggerInfoDTO {

    /**
     *
     */
    private String clientIp;

    /**
     *
     */
    private String url;

    /**
     *
     */
    private String type;

    /**
     * 请求方式GET、POST、PUT、DELETE
     */
    private String method;

    /**
     * URL后的参数
     */
    private Map<String, String[]> requestParam;

    /**
     * restful风格的路径参数
     */
    private Map<String, Object> pathVariable;

    /**
     * 请求体信息
     */
    private String requestBody;

    private String sessionId;
    
}

Customize the interceptor interceptor, judge if it is your own wrapper, and get the request parameters from the wrapper. Request parameters can only be obtained in the interceptor,

1. If request parameters are obtained in a custom Filter, restful request parameters cannot be obtained.

2. If the request parameters are obtained in AOP, the serialized request parameters are obtained (the default value of the basic type will also be obtained)

import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.example.filter.demo.dto.LoggerInfoDTO;
import com.example.filter.demo.wrapper.MyHttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

@Slf4j
public class LoggerInterceptor implements HandlerInterceptor {

        private static final ThreadLocal<LoggerInfoDTO> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  throws Exception {

        LoggerInfoDTO loggerInfo = new LoggerInfoDTO();
        // 设置请求方法,GET,POST...
        loggerInfo.setMethod(request.getMethod());
        // 设置请求sessionId
        loggerInfo.setSessionId(request.getRequestedSessionId());
        // 设置请求路径
        loggerInfo.setUrl(request.getRequestURI());
        // 设置@RequestParam注解的参数
        loggerInfo.setRequestParam(request.getParameterMap());
        // 设置restful请求参数,必须在interceptor拦截其中才能这样获取到restful参数
        Object attribute = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        if(Objects.nonNull(attribute)){
            loggerInfo.setPathVariable(JSONUtil.parseObj(attribute));
        }
        //设置客户端ip
        loggerInfo.setClientIp(ServletUtil.getClientIP(request));

        //如果是自定义wrapper, 从自定义wrapper中获取请求参数, 必须在interceptor拦截器中处理, 否则restful风格的请求参数获取不到。
        if(request instanceof MyHttpServletRequestWrapper){
            MyHttpServletRequestWrapper myHttpServletRequestWrapper = (MyHttpServletRequestWrapper) request;
            String bodyString = myHttpServletRequestWrapper.getBodyString();
            if(StringUtils.isNoneBlank(bodyString)){
                loggerInfo.setRequestBody(bodyString);
            }

        }
        //放到ThreadLocal中, 这里可以根据自己的项目业务处理
        log.info("loggerInfo : {}", JSONUtil.toJsonPrettyStr(loggerInfo));
        threadLocal.set(loggerInfo);
        return true;
    }

}

 Write interceptor configuration class

import com.example.filter.demo.interceptor.LoggerInterceptor;
import org.springframework.context.annotation.Bean;
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 InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggerInterceptor())
                // 拦截规则 ,拦截那些路径
                .addPathPatterns("/**")
                // 那些路径不拦截
                .excludePathPatterns("/user/login","/error");
    }

    @Bean
    public LoggerInterceptor loggerInterceptor() {
        return new LoggerInterceptor();
    }
}

2. Write a filter to prevent xss attacks

Enhance MyHttpServletRequestWrapper

    /**
     * @param request
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String json = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        JSONObject jsonObject = JSONUtil.parseObj(json);
        // xss过滤,HtmlUtil.filter的validateEntities方法会将双引号转码,所以不能将json整个过滤
        jsonObject.forEach((key,value) -> {
            value = HtmlUtil.filter(String.valueOf(value)).trim();
            jsonObject.set(key, value);
        });
        this.bodyString = JSONUtil.toJsonStr(jsonObject);
        this.body = this.bodyString.getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 重写获取参数进行转义
     * @param name
     * @return
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values != null) {
            int length = values.length;
            String[] escapesValues = new String[length];
            for (int i = 0; i < length; i++) {
                // 防xss攻击和过滤前后空格
                escapesValues[i] = HtmlUtil.filter(values[i]).trim();
            }
            return escapesValues;
        }
        return super.getParameterValues(name);
    }

3. Use interceptor + annotation for authentication

write annotations

import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuthCheck {
    /**
     * 授权编码
     * @return
     */
    String[] authCode();
}
import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface NoAuth {
}

Write an interceptor

import com.wulei.interceptor.demo.annotation.AuthCheck;
import com.wulei.interceptor.demo.annotation.NoAuth;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@Component
public class AuthCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // HandlerMethod 提供对方法参数、方法返回值、方法注释等的便捷访问。
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // Method 提供有关类或接口上的单个方法的信息和访问。
        Method method = handlerMethod.getMethod();


        if (method.getAnnotation(NoAuth.class) != null // 获取方法上注解
                // 获取类上注解
            || handlerMethod.getBeanType().getAnnotation(NoAuth.class) != null) {
            return true;
        }

        // TODO 获取用户角色和权限功能点

        // 判断注解功能点是否正确
        // 获取方法上注解
        AuthCheck authCheck = method.getAnnotation(AuthCheck.class);
        if (authCheck == null) {
            // 获取类上注解
            authCheck = handlerMethod.getBeanType().getAnnotation(AuthCheck.class);
        }

        String[] authCode = authCheck != null ? authCheck.authCode() : null;

        if (authCode == null || authCode.length == 0) {
            return false;
        }

        // TODO 判断用户功能点权限

        return true;
    }
}

Guess you like

Origin blog.csdn.net/m0_68705273/article/details/130086002