# 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();
}
}
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;
}
}
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;
}
}
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;
}
}
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"));
}
}
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() {
}
}
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;
}
}
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;
}
}
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);
}
}
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;
}
}
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中数据的重复读取,更简单一些。
上面拦截器和过滤器的注册省略了,按照前面的配置注册。