登录校验2.0
Filter
Filter详解
过滤器Filter在使用中的一些细节,主要介绍一下3个方面的细节:
- 过滤器的执行流程
- 过滤器的拦截路径配置
- 过滤器链
执行流程
过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用FilterChain对象当中的doFilter()方法,这个方法之前所编写的代码属于放行之前的逻辑。
在放行后访问完web资源之后还会回到过滤器当中,回到过滤器之后如果有需求还可以执行放行之后的逻辑,放行之后的逻辑写在doFilter()之后。
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("DemoFilter 放行前逻辑....");
// 放行请求
chain.doFilter(request, response);
System.out.println("DemoFilter 放行后逻辑....");
}
@Override // 销毁方法,只调用一次
public void destroy() {
System.out.println("destory 销毁方法执行了");
}
}
拦截路径
Filter可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问/login路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
拦截具体路径
@WebFilter(urlPatterns = "/login")
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("DemoFilter 放行前逻辑....");
// 放行请求
chain.doFilter(request, response);
System.out.println("DemoFilter 放行后逻辑....");
}
@Override // 销毁方法,只调用一次
public void destroy() {
System.out.println("destory 销毁方法执行了");
}
}
目录拦截
@WebFilter(urlPatterns = "/depts/*")
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("DemoFilter 放行前逻辑....");
// 放行请求
chain.doFilter(request, response);
System.out.println("DemoFilter 放行后逻辑....");
}
@Override // 销毁方法,只调用一次
public void destroy() {
System.out.println("destory 销毁方法执行了");
}
}
过滤器链
介绍
过滤器链指的是在一个Web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链
比如:在web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
而这个过滤器链上的过滤器在执行的时候回一个一个的执行,会先执行第一个filter,放行之后再来执行第二个filter,如果执行到了最后一个过滤器放行之后,才会访问对象的web资源。
访问web资源之后,按照过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行后的逻辑的时候,顺序是反着的。
先执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,在最后在给浏览器响应数据。
验证步骤:
- 在filter包下再新建一个Filter过滤器类:AbcFilter
- 在AbcFilter过滤器中编写放行前和放行后逻辑
- 配置AbcFilter过滤器拦截请求路径为:/*
- 重启SpringBoot服务,查看DemoFilter、AbcFilter的执行日志
AbcFilter过滤器
@WebFilter(urlPatterns = "/*")
public class AbcFilter 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 {
System.out.println("Abc 拦截到了请求... 放行前逻辑");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Abc 拦截到了请求...... 放行后逻辑");
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
打开浏览器访问登录接口
注意:以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。
如果想让DemoFilter先执行,就需要修改类名
登录校验-Filter
分析
登录校验的基本流程:
- 要进入到后台管理系统,必须先完成登录操作,此时就需要访问登录接口login
- 登录成功之后,需要在服务端生成一个JWT令牌,并且把JWT令牌返回给前端,前端会将JWT令牌存储下来
- 在后续的每一次请求当中,都会将JWT令牌携带到服务端,请求到达服务端之后,要想去访问对应的业务功能,此时必须先校验令牌的有效性
- 对于校验令牌的操作,使用登录校验的过滤器,在过滤器当中来校验令牌的有效性。如果令牌是无效的,就响应一个错误信息,也不会再去放行访问对应的资源。如果令牌存在,并且有效,此时就会放行去访问对应的web资源,执行相应的业务操作。
问题:
- 所有请求,拦截到了之后,都需要校验令牌吗?
- 登录请求例外
- 拦截到请求后,什么请求下才可以放行,执行业务操作?
- 有令牌,且令牌校验通过(合法);否则都返回未登录错误结果。
具体流程
基于上面的业务流程,具体的操作步骤:
- 获取请求url
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行
- 获取请求头中的令牌(token)
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析token,如果解析失败,返回错误结果(未登录)
- 放行
拦截器Interceptor
介绍
什么是拦截器?
- 是一种动态拦截方法调用的机制,类似于过滤器
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行
作用:
拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码
在拦截器中,通常也会做一些通用性的操作,如:通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器中。在校验的过程中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
拦截器的使用步骤分为两步:
- 定义拦截器
- 注册配置拦截器
自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
// 目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
// 视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
注意:
- preHandle方法:目标资源方法执行前执行。返回true:放行;返回false:不放行
- postHandle方法:目标资源方法执行后执行
- afterCompletion方法:视图渲染完毕后执行,最后执行
注册配置拦截器:实现WebMvcConfigure接口,并重写addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 自定义的拦截器对象
@Resource
private LoginCheckInterceptor loginCheckInterceptor;
// 注册自定义拦截器对象
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置拦截器拦截的请求路径(/** 表示拦截所有请求)
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
效果如图:
如果将拦截器中返回值改为false,再次发送请求后,没有响应数据,说明请求被拦截了没有放行
Interceptor详解
拦截器的使用细节:
- 拦截器的拦截路径配置
- 拦截器的执行流程
拦截路径
在注册配置拦截器的时候,要指定拦截器的路径,通过addPathOatterns("要拦截路径")
方法,就可以指定要拦截哪些资源
/**
表示拦截所有资源,而在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns("不拦截路径")
方法,指定哪些资源不需要拦截
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 自定义的拦截器对象
@Resource
private LoginCheckInterceptor loginCheckInterceptor;
// 注册自定义拦截器对象
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置拦截器拦截的请求路径(/** 表示拦截所有请求)
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**").excludePathPatterns("/login");
}
}
在拦截器中除了可以设置/**
拦截所有资源外,还有一些常见拦截路径设置:
拦截路径 | 含义 | 例子 |
---|---|---|
/* | 一级路径 | 能匹配/depts、/emps、/login,不能匹配/depts/1 |
/** | 任意级路径 | 能匹配/depts、/depts/1、/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配/depts/1、不能匹配/depts/1/2、/depts |
/depts/** | /depts下的任意级路径 | 能匹配/depts、/depts/1、/depts/1/2、不能匹配/emps/1 |
执行流程
介绍
- 当打开浏览器来访问部署在web服务器当中的web应用时,此时定义的拦截器会拦截到这次请求。拦截到这次请求之后,会先执行放行前的逻辑,然后再执行放行操作。由于是基于Springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
- Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servelt:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
- 当定义了拦截器之后,会在执行Controller的方法之前,请求被拦截器拦截住。执行
preHandle()
方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)- 在controller当中的方法执行完毕之后,再回来执行
postHandle()
这个方法以及afterCompletion()
方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分。执行完毕之后,最终给浏览器响应数据。
拦截器和过滤器的区别
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口
- 拦截范围不同:过滤器Filter会拦截所有资源,而Interceptor只会拦截环境中的资源
登录校验——Interceptor
登录校验的业务逻辑和登录校验Filter过滤器当中的逻辑是完全一致的。
登录校验拦截器
//自定义拦截器
@Component //当前拦截器对象由Spring创建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
//前置方式
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
// 1.获取请求url
// 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
// 3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);
// 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");
// 创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
// 把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
// 设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
// 响应
response.getWriter().write(json);
return false;//不放行
}
// 5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");
// 创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
// 把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头
response.setContentType("application/json; charset=utf-8");
//响应
response.getWriter().write(json);
return false;
}
//6.放行
return true;
}
注册配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry
.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
当用户没有登录,校验机制返回错误信息,前端页面根据返回的错误信息结果,自动跳转到登录页面
异常处理
当出现异常的时候,并且我们没有做任何异常处理的时候,三层架构处理异常的方案:
- Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就给谁抛),会抛给Service
- Service中也存在异常了,会抛给controller
- 而在controller当中,没有做任何的异常处理,所以最终异常会再往上抛,最终会抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误信息,但是框架返回的JSON格式的数据并不符合开发规范。
解决方案:
在三层架构项目中,出现了异常,该如何处理?
- 方案一:在所有Controller的所有方法中进行try…catch处理
- 缺点:代码臃肿(不推荐)
- 方案二:全局异常处理器
- 好处:简单、优雅(推荐)
全局异常处理器
流程:
- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注释@RestControllerAdvice,加上这个注释就代表定义了一个全局异常处理器
- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定要捕获的是哪一类型的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody处理异常的方法返回值会转换为json后响应给前端
全局异常处理器的使用,主要涉及到两个注解:
- @RestControllerAdvice // 表示当前类为全局异常处理器
- @ExceptionHandler // 指定可以捕获哪种类型的异常进行处理