拦截器:Interceptor
上文提到,可以使用Servlet中的组件——Filter过滤器进行统一拦截,实现登录认证的功能。但在Spring框架中,也提供了一个类似于Filter过滤器的东西,那就是——拦截器Interceptor,拦截器是一种动态拦截方法调用的机制,其逻辑和Filter过滤器类似。
其主要作用也是拦截请求,在请求到服务端之前进行拦截,根据需求执行预先设定的代码。在拦截器中,一般也是做一些通用性操作,比如说实现登录校验,其实现逻辑和Filter过滤器类似,此处不做赘述 (详情请看:登录认证(5):过滤器:Filter)
拦截器快速入门
拦截器的快速入门也分两步:定义拦截器和注册配置拦截器,这也是和Filter过滤器相似的地方。
自定义拦截器
自定义拦截器需要定义一个类,并实现HandlerInterceptor接口,并重写其中所有方法:
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
// 访问目标资源之前执行,返回true:放行 false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
return true; // 放行
}
// 目标资源方法执行之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("postHandle...");
}
// 视图渲染完成之后执行,是最后执行的
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
log.info("afterCompletion...");
}
}
preHandle方法
preHandle方法,在请求目标资源方法执行之前执行,其boolean类型的返回值代表了是否放行,如果返回true,则放行;返回false,则不放行。
postHandle方法
postHandle方法,在请求目标资源方法执行之后执行,可以根据需求,在请求返回客户端之前,进行额外的处理。
afterCompletion方法
afterCompletion方法,在视图渲染完毕之后执行,在早期开发中,没有分前后端,前端的视图渲染也需要后端服务器完成,afterCompletion方法会在视图渲染完成之后执行。但是现在前端的视图渲染是由单独的前端服务器完成,所以说这个方法了解即可。
自定义拦截器之后,现在该拦截器还是不能工作的,还需要注册配置拦截器。首先需要创建一个配置类,配置类需要实现WebMvcConfigurer接口,并重写addInterceptors方法,在addInterceptors方法中,需要注册自定拦截器对象,并且设置拦截器拦截的请求路径。
/**
* 配置类,配置Interceptor拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 定义需要配置的拦截器对象
private final DemoInterceptor demoInterceptor;
@Autowired
public WebConfig(DemoInterceptor demoInterceptor) {
this.demoInterceptor = demoInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(demoInterceptor).addPathPatterns("/**"); // 设置拦截路径
}
}
由于我们给自定义的DemoInterceptor拦截器对象添加了@Compnent注解,使其加入了IOC容器,所以说我们可以在配置类中直接使用@AutoWired注解进行依赖注入。在addInterceptors方法中,通过InterceptorRegistry类的addInterceptor方法,注册自定义拦截器对象,并在addPathPatterns方法中设置拦截路径,这样,拦截器的配置就完成了,拦截器就可以正常工作了。此时发起请求测试(此时建议将前面配置的过滤器关闭 ):
我们看到,因为在preHandle方法中直接返回了true,所以说所有的请求都会直接放行,所以说就可以成功访问到服务器资源,并且这三个方法的运行顺序,也和我们分析的一致。
登录校验Interceptor
讲解完Interceptor拦截器的快速入门之后,我们需要仿造Filter过滤器的逻辑,使用Interceptor拦截器,完成登录校验的功能。其逻辑和实现步骤在介绍Filter过滤器的时候已经讲解过,二者的逻辑是完全一致的,所以说此处不做赘述,只需要将原来的技术方案从过滤器换成拦截器即可:
/**
* 登录拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取URL,如果是登录,那么放行
String url = request.getRequestURL().toString();
if (url.contains("/login")) {
log.info("登录请求,放行...");
return true;
}
// 获取token
String token = request.getHeader("token");
// 如果token为null,表示未登录,返回401
if (token == null || token.isEmpty()) {
log.info("未登录,不放行");
response.setStatus(401);
return false;
}
// 解析token,如果无法解析,说明token错误,登录失败,返回401
try {
Claims claims = JWTUtils.parseJWT(token);
ThreadUtils.setCurrentId((Integer)claims.get("id"));
} catch (Exception e) {
e.printStackTrace();
log.info("token解析失败,登录失败,不放行");
response.setStatus(401);
return false;
}
// 如果token解析成功,说明登录成功,可以放行
log.info("token成功解析,登录成功,放行");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
// 清空当前线程的存储空间
ThreadUtils.remove();
}
}
配置拦截器:
/**
* 配置类,配置Interceptor拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 定义需要配置的拦截器对象
private final LoginInterceptor loginInterceptor;
@Autowired
public WebConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); // 设置拦截路径
}
}
配置完毕后,启动服务,发起请求进行测试:
此时用户没有登录,自然没有JWT令牌,发起请求被拦截器拦截,不会放行。
此时发起登录请求,根据我们的逻辑:登录请求直接放行,所以说请求成功访问服务器资源,获得了JWT令牌并返回,后续的请求都要携带JWT令牌进行访问。
携带正确的JWT令牌发起请求,成功访问到服务器资源。
篡改JWT令牌,解析失败,请求被拦截,根据以上测试结果,说明我们配置的登录拦截器成功运行。
Interceptor详解
拦截路径
在注册配置拦截器的时候,我们是通过addPathPattern("需要拦截的路径")完成拦截路径配置的,但是Interceptor拦截器十分的智能,不但可以指定需要拦截的资源,还可以指定不拦截的资源,需要使用excludePathPattern("不拦截的路径")方法,指定不拦截的资源:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // 设置拦截路径
}
这样,登录请求就不会再拦截了,就不再需要在拦截方法中进行判断了。
拦截器除了可以使用/**拦截所有资源外,还可以根据需求进行不同的拦截路径设置,其中常见的有:/*是拦截一级路径,可以拦截/depts、/emps等类似的路径,但是只能拦截到一级,不能匹配/depts/1这样的路径;/**是拦截任意级路径,所有路径都会被拦截;/depts/*是拦截/depts下的一级路径,可以匹配/depts/1这样的路径,但是无法匹配到/depts/1/2或/depts这样的路径;/depts/**是拦截/depts下的任意级路径,如/depts、depts/1/2、depts/1这样的路径,但是不能匹配到/emps这样的路径。
执行流程
拦截器大致执行流程如图所示:
如图所示,当发起请求时,假如服务器同时存在Filter过滤器和Interceptor拦截器,会先执行Filter过滤器,这其实很好解释,因为过滤器是原生的Servlet中的组件,而拦截器是Spring框架中提供的技术,换句话说:Interceptor拦截器只会拦截Spring环境中的资源,而过滤器会拦截所有的资源。
Tomcat服务器并不识别Spring框架中的Controller程序,只识别Servlet程序,所以说Spring框架必须依赖一个核心的Servlet——DispatcherServlet(前端控制器),前端发起的所有请求,都会先经过DispatcherServlet,再由其将请求转给Controller,在转给Controller之前,如果定义了拦截器,那么就会被拦截器拦截,执行preHandler方法,方法的返回值是一个boolean类型,真假代表了是否拦截器是否放行。当Controller中的方法执行完毕之后,再会回到拦截器中执行postHandle和afterCompletion方法,然后会回到Filter过滤器中,执行放行后逻辑,最后才给前端响应数据。
总结
原生Servlet提供的Filter过滤器和Spring框架提供的Interceptor拦截器都可以在请求到达web服务器之前,对请求进行统一拦截,二者的功能相似,实现略有差别,需要根据不同的需求进行使用。并且Interceptor拦截器只会拦截Spring环境中的资源,而Filter过滤器的拦截范围为所有资源,范围更大。并且由于Spring系列框架越来越广泛,Interceptor拦截器的使用高于Filter过滤器。