1 Interceptor 简介
拦截器在Web系统中非常常见,对于某些全局统一的操作,我们可以把它提取到拦截器中实现。
在 Spring MVC 中使用接口 HandlerInterceptor 表示,包含三个方法:
- preHandle() 在请求处理之前执行,允许您在请求被传递给控制器之前进行一些预处理操作,
比如鉴权、日志记录等。如果返回true,则继续执行请求处理链;如果返回false,则中断请求处理。
- postHandle() 在请求处理之后、视图渲染之前执行。您可以在这里对模型和视图进行操作,
但不能改变视图。通常用于修改响应内容、添加额外的模型数据等。
- afterCompletion() 在整个请求完成后执行,包括视图渲染。
可以用于清理资源、记录请求处理时间等操作。
三个方法都有的 handler参数 表示处理器,通常情况下可以表示我们使用注解 @Controller
定义的控制器。
一个请求完整的流程图 及 Interceptor的执行顺序:
注意:
自从前后端分离之后,Spring MVC 中的处理器方法执行后通常不会再返回视图,而是返回表示 json 或 xml 的对象,@Controller 方法返回值类型如果为 ResponseEntity 或标注了 @ResponseBody 注解,此时处理器方法一旦执行结束,Spring 将使用 HandlerMethodReturnValueHandler 对返回值进行处理,会将返回值转换为 json 或 xml,然后写入响应,后续也不会进行视图渲染,这时postHandle 将没有机会修改响应体内容。
如果需要更改响应内容,可以定义一个实现 ResponseBodyAdvice 接口的类,然后将这个类直接定义到 RequestMappingHandlerAdapter 中的 requestResponseBodyAdvice 或通过 @ControllerAdvice 注解添加到 RequestMappingHandlerAdapter。
那么拦截器的顺序是如何指定的呢?
对于 xml 配置来说,Spring 将记录 bean 声明的顺序,先声明的拦截器将排在前面。 对于注解配置来说,由于通过反射读取方法无法保证顺序,因此需要在方法上添加@Order注解指定 bean 的声明顺序。 对应API配置来说,拦截器的顺序并非和添加顺序完全保持一致,为了控制先后顺序,需要自定义的拦截器实现Ordered接口。
注解配置指定顺序示例如下:
@Configuration
public class MvcConfig {
@Order(2)
@Bean
public MappedInterceptor loginInterceptor() {
return new MappedInterceptor(
new String[]{"/**"}, new String[]{"/login"},
new LoginInterceptor()
);
}
@Order(1)
@Bean
public MappedInterceptor logInterceptor() {
return new MappedInterceptor(null, new LoginInterceptor());
}
}
此时虽然登录拦截器写在前面,但因为 @Order 注解指定的值较大,因此将排在日志拦截器的后面。
API配置指定顺序示例如下:
public class LoginInterceptor implements HandlerInterceptor, Ordered {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("已登录");
return true;
}
@Override
public int getOrder() {
return 2;
}
}
public class LogInterceptor implements HandlerInterceptor, Ordered {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("请求来了");
return true;
}
@Override
public int getOrder() {
return 1;
}
}
LogInterceptor 指定的排序号较 LoginInterceptor 来说比较小,因此 LogInterceptor 将排在前面。
2 Interceptor 应用场景
过滤器应用场景
- 登录验证,判断用户是否登录;
- 权限验证,判断用户是否有权限访问资源,如校验token;
- 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量;
- 处理cookie、本地化、国际化、主题等;
- 性能监控,监控请求处理时长等;
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)
拦截器应用场景
- 字符编码设置;
- 响应数据压缩;
- URL级别的权限访问控制
- 过滤敏感词汇(防止sql注入)
3 Interceptor 与Filter(过滤器)的区别
过滤器 和 拦截器 都是 AOP的编程思想 的一种体现,用来解决项目中某一类问题的两种接口(工具),都可以对请求做一些增强。
过滤器配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,
看到Filter 接口中定义了三个方法:
- init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。
**注意:**这个方法必须执行成功,否则过滤器会不起作用。
- doFilter() :容器中的每一次请求都会调用该方法,FilterChain 用来调用下一个过滤器Filter。
- destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,
在过滤器 Filter 的整个生命周期也只会被调用一次。
@Component
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 前置");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 处理中");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("Filter 后置");
}
}
3.1 实现原理不同
过滤器 是基于 函数回调 实现的;拦截器 是基于Java的 **反射机制(动态代理)**实现的。
重点说下过滤器:
在我们自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,
而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个
doFilter() 方法就是回调方法。
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter()
里调用各个自定义xxxFilter过滤器,并执行 doFilter() 方法。
public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response) {
...//省略
internalDoFilter(request,response);
}
private void internalDoFilter(ServletRequest request, ServletResponse response){
if (pos < n) {
//获取第pos个filter
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
...
filter.doFilter(request, response, this);
}
}
}
而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行
filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain
的doFilter() 方法,以此循环执行实现函数回调。
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
3.2 使用范围不同
过滤器 实现的是 javax.servlet.Filter
接口,而这个接口是在Servlet
规范中定义的,也就是说过滤器Filter
的使用要依赖于Tomcat
等容器,导致它只能在web
程序中使用。
拦截器(Interceptor
) 它是一个Spring
组件,并由Spring
容器管理,并不依赖Tomcat
等容器,是可以单独使用的。不仅能应用在web
程序中,也可以用于Application
、Swing
等程序中。
3.3 触发时机不同
过滤器
和 拦截器
的触发时机也不同,我们看下边这张图:
4 自定义Interceptor 实现方式
4.1 实现 HandlerInterceptor接口
// 最常见实现方式
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
// ...
}
4.2 继承 HandlerInterceptorAdapter类
HandlerInterceptorAdapter
是 HandlerInterceptor
接口 的适配器类,通过继承它可以实现拦截器。
4.3 使用 @Component注解 实现自动注册
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
// ...
}
注: 方式一、方式二均需要手动 将自定义好的拦截器处理类进行注册。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//定义 放行路径
String[] swaggerExcludes = new String[]{
"/login","/swagger**/**","/webjars/**","/v3/**","/doc.html"
};
// 多个拦截器组成一个拦截器链
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**")// addPathPatterns 添加拦截规则,/**表示拦截所有请求
.excludePathPatterns(swaggerExcludes);// excludePathPatterns 排除拦截
}
//springboot2.x 静态资源在自定义拦截器之后无法访问的解决方案
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
}
5 SpringBoot 2. 配置 Interceptor(拦截器) 并 集成 Swagger2 3.0*
5.1 引入依赖
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'
5.2 Swagger 配置
@Slf4j
@EnableOpenApi
@Configuration
public class SwaggerConfig {
/**
* 是否开启swagger,正式环境一般是需要关闭的,可根据springboot的多环境配置进行设置
*/
@Value("${swagger.enabled:false}")
Boolean swaggerEnabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30) // V3
// 是否开启
.enable(swaggerEnabled)
.apiInfo(apiInfo())
.select()
// 扫描的路径包
.apis(RequestHandlerSelectors.basePackage("com.baseframe.core.controller"))
// 指定路径处理PathSelectors.any()代表所有的路径
.paths(PathSelectors.any())
.build()
.pathMapping("/");
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("BaseFrame 接口文档")
.description("BaseFrame Restful 接口")
.contact(new Contact("zhuzs", "<https://xxx.com>", "XXX@qq.com"))
.version("1.0.0")
.build();
}
}
5.3 自定义拦截器
@Slf4j
public class AuthInterceptor implements HandlerInterceptor, Ordered {
@Autowired
private AuthService authService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
log.info("开始验证权限...");
String token = TokenUtil.getRequestToken(request);
//如果token为空
if (StringUtils.isBlank(token)) {
setResponse(response,400,"用户未登录,请先登录");
return false;
}
//1. 根据token,查询用户信息
UserEntity userEntity = authService.findByToken(token);
//2. 若用户不存在,
if (userEntity == null) {
setResponse(response,400,"用户不存在");
return false;
}
//3. token失效
if (userEntity.getExpireTime().isBefore(LocalDateTime.now())) {
setResponse(response,400,"用户登录凭证已失效,请重新登录");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
//返回错误信息
private static void setResponse(HttpServletResponse response, int status, String msg) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
//UTF-8编码
httpResponse.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
Result build = Result.build(status, msg);
String json = JSON.toJSONString(build);
httpResponse.getWriter().print(json);
}
@Override
public int getOrder() {
return 2;
}
}
5.4 将自定义好的拦截器处理类进行注册,并通过addPathPatterns、excludePathPatterns等属性设置需要拦截或需要排除的 URL。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//定义 放行路径
String[] swaggerExcludes = new String[]{
"/login",
"/swagger**/**",
"/webjars/**",
"/v3/**",
"/doc.html"
};
// 多个拦截器组成一个拦截器链
// addPathPatterns 用于添加拦截规则,/**表示拦截所有请求
// excludePathPatterns 用户排除拦截
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(swaggerExcludes);
}
//springboot2.x 静态资源在自定义拦截器之后无法访问的解决方案
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
}
参考文章:
Spring MVC 系列之拦截器 Interceptor 最全总结_springmvc interceptor_大鹏cool的博客-CSDN博客