# SpringBoot3教程 - 13 过滤器配置

过滤器是基于 Servlet 规范的组件,它们在 Servlet 容器(例如 Tomcat)中运行,用于拦截和处理进入应用程序的 HTTP 请求和响应。过滤器可以在请求到达 Servlet 之前和响应返回客户端之前对请求和响应进行预处理或后处理。

# 13.1 拦截器和过滤器的区别

先查看一张图,看一下请求的执行的流程:

请求到达服务器,先会由 Filter 拦截处理,然后交由 servlet 处理,也就由 SpringMVC 的 DispatcherServlet 拦截请求,DispatcherServlet 使用 HandlerMapping 解析请求 URL 并找到对应的 controller ,在执行 controller 方法之前,会执行一个或多个拦截器进行一些预处理,然后才执行 controller 中的方法,controller 根据请求中的信息执行相应的业务逻辑,并返回一个ModelAndView对象,包含了处理请求后的数据和视图的名称。在 controller 执行之后,但在视图渲染之前,拦截器(postHandle()方法)可以执行一些后处理操作,在请求处理完毕之后(包括视图渲染),拦截器(afterCompletion()方法)还可以执行一些清理工作。DispatcherServlet 将结果返回给客户端。

拦截器和过滤器主要有如下区别:

  • 过滤器是 Servlet 层的组件,拦截器是 Spring MVC 层的组件。
  • 过滤器可以处理任何请求(包括静态资源),而拦截器只能拦截Controller方法的调用。
  • 过滤器在请求到达 Servlet 之前和响应返回客户端之前执行,拦截器在控制器方法调用之前、之后以及请求处理完成后执行。
  • 过滤器只能对 request 和 response 进行操作,而拦截器可以对 request、response、handler、modelAndView、exception进行操作。

一般使用拦截器处理和业务相关的处理,例如检查用户登录、是否有访问权限等,

# 13.2 过滤器的配置

# 1 创建过滤器

创建类,实现 jakarta.servlet.Filter 接口。

package com.doubibiji.hellospringboot.filter;


import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;

@Slf4j
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 过滤器逻辑
        log.info("MyFilter doFilter");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

有人同时在 Filter 类上使用@WebFilter注解和@Component注解来注册一个过滤器,那种方式不太推荐。当然如果你使用@WebFilter注解搭配@ServletComponentScan注解使用也是可以的,这里不介绍了。下面使用 FilterRegistrationBean 来注册过滤器,这种方式更为灵活。

# 2 注册过滤器

过滤器已经创建了,下面需要配置过滤器,例如拦截哪些请求和顺序。

首先创建一个配置类,在配置类中通过 FilterRegistrationBean 注册过滤器。

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.filter.MyFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {  // 注册过滤器
        FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MyFilter());
        registrationBean.addUrlPatterns("/*");  // 设置拦截路径,如果要拦截多个路径,可以传入集合
        registrationBean.setName("myFilter");  // 设置拦截器名称
        registrationBean.setOrder(1);  // 设置过滤器顺序,值越小优先级越高
        return registrationBean;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 13.3 解决Request body读取一次的问题

前面介绍可以使用拦截器拦截请求,验证请求的数据。但是如果如果进行读取请求的 body 数据的时候,可能会遇到一个问题。

举个栗子:

首先有一个接口,接收 POST 请求,接收 JSON 格式的 body 数据。

package com.doubibiji.hellospringboot.controller;

import com.doubibiji.hellospringboot.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class UserController {

    @PostMapping("/saveUser")
    public UserDto saveUser(@RequestBody UserDto userDto) {
        log.info("请求的数据:{}", userDto);
        return userDto;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的接口,使用 postman 工具测试一下,是可以获取到 UserDto 的数据的。

但是我们可能会有一些需求,使用拦截器拦截请求,然后查看 body 中的数据是否满足指定的条件,例如是否登录,就需要在拦截器中获取 body 中的数据。

下面定义一个拦截器,获取body中的数据:

package com.doubibiji.hellospringboot.filter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@Slf4j
@Component
public class DoubiSignAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 尝试读取请求体
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        BufferedReader bufferedReader = null;

        try {
            InputStream inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line.trim());
                }
            }
        } catch (IOException e) {
            // 处理异常
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // 检查是否读取到了内容(很可能为空或不完整)
        String body = stringBuilder.toString();

        log.info("请求的body:{}", body);
        
        // ...获取body后的处理

        return true;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

然后在配置类中注册拦截器(这里就省略了),结果发现再次请求后,报错(HttpMessageNotReadableException),controller 中的接口无法获取到数据了。

这是为什么呢?

request 里的 body 是以字节流的方式读取的,默认情况下读取一次,字节流就没有了,所以在拦截器中读取会导致后续 controller@RequestBody 注解无法获取参数。

如果遇到这个问题,可以使用过滤器对 request 进行包装。

# 1 定义RequestWrapper

接收 HttpServletRequest ,将其中的数据读取出来并缓存。

package com.doubibiji.hellospringboot.filter;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.*;
import java.nio.charset.Charset;

/**
 * 继承HttpServletRequestWrapper类,将请求体中的内容copy出来,覆写getInputStream()和getReader()方法供外部使用
 * 每次调用覆写后的getInputStream()方法都是从复制出来的二进制数组中进行获取
 */
public class DoubiRequestWrapper extends HttpServletRequestWrapper {

    // 读取body的内容缓存
    private final String body;

    public DoubiRequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(Charset.forName("UTF-8")));
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(),"UTF-8"));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

# 2 创建过滤器

创建过滤器拦截请求,将 ServletRequest 转换为 DoubiRequestWrapper

package com.doubibiji.hellospringboot.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
public class RequestWrapFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
        ServletRequest requestWrapper = null;
        if(servletRequest instanceof HttpServletRequest) {
            // 转换为DoubiRequestWrapper
            requestWrapper = new DoubiRequestWrapper((HttpServletRequest) servletRequest);
        }

        if (requestWrapper == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    }

    @Override
    public void destroy() {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 3 修改拦截器

修改拦截器中获取 body 数据的部分,通过包装的 DoubiRequestWrapper 来获取。

package com.doubibiji.hellospringboot.filter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class DoubiSignAuthInterceptor implements HandlerInterceptor {

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

        String body = null;
        if (request instanceof DoubiRequestWrapper) {
            DoubiRequestWrapper requestWrapper = (DoubiRequestWrapper) request;
            body = requestWrapper.getBody();
        }

        log.info("请求的body:" + body);
        
        // ...获取body后的处理

        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 4 注册过滤器和拦截器

在配置类中注册过滤器和拦截器。

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.filter.DoubiSignAuthInterceptor;
import com.doubibiji.hellospringboot.filter.RequestWrapFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
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;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired  // 注入拦截器
    private DoubiSignAuthInterceptor signAuthInterceptor;

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 根据需要取消拦截指定的请求
        List<String> excludePathList = new ArrayList<>();
        excludePathList.add("/login");
        excludePathList.add("/register");
        excludePathList.add("/error");

        // 拦截所有请求,排除指定请求
        registry.addInterceptor(signAuthInterceptor)
                .addPathPatterns("/**").excludePathPatterns(excludePathList);
    }

    /**
     * 注册过滤器
     */
    @Bean
    public FilterRegistrationBean<RequestWrapFilter> myFilter() {
        FilterRegistrationBean<RequestWrapFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new RequestWrapFilter());
        registrationBean.addUrlPatterns("/*");  // 设置拦截路径,如果要拦截多个路径,可以传入集合
        registrationBean.setName("requestWrapFilter");  // 设置拦截器名称
        registrationBean.setOrder(1);  // 设置过滤器顺序,值越小优先级越高
        return registrationBean;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

再次请求接口,发现拦截器和 controller 中可以正常获取到数据了。

# 13.4 另一种解决body读取一次的方法

可以使用 ContentCachingRequestWrapper 对请求进行包装,ContentCachingRequestWrapper是 Spring 提供的一个类,它包装了原始的 HttpServletRequest,并允许你读取请求体多次。它内部使用了一个字节数组缓冲区来存储请求体的内容。

你可以在你的配置中或者过滤器(Filter)中包装请求对象,以便在整个请求处理过程中都可以多次读取请求体。

# 1 定义一个过滤器

定义过滤器,在过滤器中使用 ContentCachingRequestWrapper 对请求进行包装:

package com.doubibiji.hellospringboot.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.io.IOException;

@Slf4j
public class RequestBodyCachingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 使用ContentCachingRequestWrapper包装请求
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrappedRequest, response);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2 在拦截器中获取数据

在拦截器中获取 body 中的数据。

package com.doubibiji.hellospringboot.filter;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.Charset;

@Slf4j
@Component
public class DoubiSignAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String body = null;
        if (request instanceof ContentCachingRequestWrapper) {
            // 通过 ContentCachingRequestWrapper 来读去数据
            ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request;
            if (wrappedRequest.getInputStream().isFinished()) {
                body = wrappedRequest.getContentAsString();
            }
            else {
                body = IoUtil.read(wrappedRequest.getInputStream(), Charset.forName("UTF-8"));
            }
        }

        log.info("请求的body:" + body);
        
        // ...获取body后的处理

        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

有个地方需要注意,如果 ContentCachingRequestWrapper 的输入流如果没有被读取过,那么通过其调用 getContentAsString()getContentAsByteArray() 方法是无法获取到数据的。

通过这种方式,也可以实现body中数据的重复读取,更简单一些。

上面拦截器和过滤器的注册省略了,按照前面的配置注册。