当前位置:   article > 正文

SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回...

springboot 全局判断用户是否有登陆

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:blog.csdn.net/m0_58761900/

article/details/129012784


本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节

  • 用户登录权限的校验实现接口 HandlerInterceptor + WebMvcConfigurer

  • 异常处理使用注解 @RestControllerAdvice + @ExceptionHandler

  • 数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice

1. 统一用户登录权限效验

用户登录权限的发展完善过程

  • 最初用户登录效验: 在每个方法中获取 Session 和 Session 中的用户信息,如果存在用户,那么就认为登录成功了,否则就登录失败了

  • 第二版用户登录效验: 提供统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断

  • 第三版用户登录效验: 使用 Spring AOP 来统一进行用户登录效验

  • 第四版用户登录效验: 使用 Spring 拦截器来实现用户的统一登录验证

1.1 最初用户登录权限效验

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4.     @RequestMapping("/a1")
  5.     public Boolean login (HttpServletRequest request) {
  6.         // 有 Session 就获取,没有就不创建
  7.         HttpSession session = request.getSession(false);
  8.         if (session != null && session.getAttribute("userinfo") != null) {
  9.             // 说明已经登录,进行业务处理
  10.             return true;
  11.         } else {
  12.             // 未登录
  13.             return false;
  14.         }
  15.     }
  16.     @RequestMapping("/a2")
  17.     public Boolean login2 (HttpServletRequest request) {
  18.         // 有 Session 就获取,没有就不创建
  19.         HttpSession session = request.getSession(false);
  20.         if (session != null && session.getAttribute("userinfo") != null) {
  21.             // 说明已经登录,进行业务处理
  22.             return true;
  23.         } else {
  24.             // 未登录
  25.             return false;
  26.         }
  27.     }
  28. }

这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:

  • 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断

  • 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功

  • 这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 AOP 方法来进行统一的用户登录权限验证是非常好的解决办法。

1.2 Spring AOP 统一用户登录验证

统一用户登录验证,首先想到的实现方法是使用 Spring AOP 前置通知或环绕通知来实现

  1. @Aspect // 当前类是一个切面
  2. @Component
  3. public class UserAspect {
  4.     // 定义切点方法 Controller 包下、子孙包下所有类的所有方法
  5.     @Pointcut("execution(* com.example.springaop.controller..*.*(..))")
  6.     public void  pointcut(){}
  7.     
  8.     // 前置通知
  9.     @Before("pointcut()")
  10.     public void doBefore() {}
  11.     
  12.     // 环绕通知
  13.     @Around("pointcut()")
  14.     public Object doAround(ProceedingJoinPoint joinPoint) {
  15.         Object obj = null;
  16.         System.out.println("Around 方法开始执行");
  17.         try {
  18.             obj = joinPoint.proceed();
  19.         } catch (Throwable e) {
  20.             e.printStackTrace();
  21.         }
  22.         System.out.println("Around 方法结束执行");
  23.         return obj;
  24.     }
  25. }

但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:

  • 没有办法得到 HttpSession 和 Request 对象

  • 我们要对一部分方法进行拦截,而另一部分方法不拦截,比如注册方法和登录方法是不拦截的,也就是实际的拦截规则很复杂,使用简单的 aspectJ 表达式无法满足拦截的需求

1.3 Spring 拦截器

针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:

1.创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法

2.将自定义拦截器加入到框架的配置中,并且设置拦截规则

  • 给当前的类添加 @Configuration 注解

  • 实现 WebMvcConfigurer 接口

  • 重写 addInterceptors 方法

注意:一个项目中可以同时配置多个拦截器

(1)创建自定义拦截器
  1. /**
  2.  * @Description: 自定义用户登录的拦截器
  3.  * @Date 2023/2/13 13:06
  4.  */
  5. @Component
  6. public class LoginIntercept implements HandlerInterceptor {
  7.     // 返回 true 表示拦截判断通过,可以访问后面的接口
  8.     // 返回 false 表示拦截未通过,直接返回结果给前端
  9.     @Override
  10.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
  11.                              Object handler) throws Exception {
  12.         // 1.得到 HttpSession 对象
  13.         HttpSession session = request.getSession(false);
  14.         if (session != null && session.getAttribute("userinfo") != null) {
  15.             // 表示已经登录
  16.             return true;
  17.         }
  18.         // 执行到此代码表示未登录,未登录就跳转到登录页面
  19.         response.sendRedirect("/login.html");
  20.         return false;
  21.     }
  22. }
(2)将自定义拦截器添加到系统配置中,并设置拦截的规则
  • addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法

  • excludePathPatterns:表示需要排除的 URL

说明:拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。

  1. /**
  2.  * @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则
  3.  * @Date 2023/2/13 13:13
  4.  */
  5. @Configuration
  6. public class AppConfig implements WebMvcConfigurer {
  7.     @Resource
  8.     private LoginIntercept loginIntercept;
  9.     @Override
  10.     public void addInterceptors(InterceptorRegistry registry) {
  11. //        registry.addInterceptor(new LoginIntercept());//可以直接new 也可以属性注入
  12.         registry.addInterceptor(loginIntercept).
  13.                 addPathPatterns("/**").    // 拦截所有 url
  14.                 excludePathPatterns("/user/login"). //不拦截登录注册接口
  15.                 excludePathPatterns("/user/reg").
  16.                 excludePathPatterns("/login.html").
  17.                 excludePathPatterns("/reg.html").
  18.                 excludePathPatterns("/**/*.js").
  19.                 excludePathPatterns("/**/*.css").
  20.                 excludePathPatterns("/**/*.png").
  21.                 excludePathPatterns("/**/*.jpg");
  22.     }
  23. }

1.4 练习:登录拦截器

要求

  • 登录、注册页面不拦截,其他页面都拦截

  • 当登录成功写入 session 之后,拦截的页面可正常访问

在 1.3 中已经创建了自定义拦截器 和 将自定义拦截器添加到系统配置中,并设置拦截的规则

(1)下面创建登录和首页的 html

03e6422578c3255f4393e57345752022.png

(2)创建 controller 包,在包中创建 UserController,写登录页面和首页的业务代码

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4.     @RequestMapping("/login")
  5.     public boolean login(HttpServletRequest request,String username, String password) {
  6.         boolean result = false;
  7.         if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
  8.             if(username.equals("admin") && password.equals("admin")) {
  9.                 HttpSession session = request.getSession();
  10.                 session.setAttribute("userinfo","userinfo");
  11.                 return true;
  12.             }
  13.         }
  14.         return result;
  15.     }
  16.     @RequestMapping("/index")
  17.     public String index() {
  18.         return "Hello Index";
  19.     }
  20. }

(3)运行程序,访问页面,对比登录前和登录后的效果

77a9a2e25b4fba4475e2775fe04ad44c.png dd095f15ffc13e16d485b8a2ce3a36e0.png

1.5 拦截器实现原理

有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示

e2954025328f0af811439d5db391b1ba.png

实现原理源码分析

所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现

5457c9470461edce9f001be9bb849567.png

而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码分析如下:

80ff3081dba4f4b40e8e8a12ef224d62.png

通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的

1.6 统一访问前缀添加

所有请求地址添加 api 前缀,c 表示所有

  1. @Configuration
  2. public class AppConfig implements WebMvcConfigurer {
  3.     // 所有的接口添加 api 前缀
  4.     @Override
  5.     public void configurePathMatch(PathMatchConfigurer configurer) {
  6.         configurer.addPathPrefix("api", c -> true);
  7.     }
  8. }
7812a641586d5b192b0fc0612a64b6e0.png

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

2. 统一异常处理

给当前的类上加 @ControllerAdvice 表示控制器通知类

给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4.     @RequestMapping("/index")
  5.     public String index() {
  6.         int num = 10/0;
  7.         return "Hello Index";
  8.     }
  9. }

在 config 包中,创建 MyExceptionAdvice

  1. @RestControllerAdvice // 当前是针对 Controller 的通知类(增强类)
  2. public class MyExceptionAdvice {
  3.     @ExceptionHandler(ArithmeticException.class)
  4.     public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
  5.         HashMap<String, Object> result = new HashMap<>();
  6.         result.put("state",-1);
  7.         result.put("data",null);
  8.         result.put("msg" , "算出异常:"+ e.getMessage());
  9.         return result;
  10.     }
  11. }

也可以这样写,效果是一样的

  1. @ControllerAdvice
  2. public class MyExceptionAdvice {
  3.     @ExceptionHandler(ArithmeticException.class)
  4.     @ResponseBody
  5.     public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
  6.         HashMap<String, Object> result = new HashMap<>();
  7.         result.put("state",-1);
  8.         result.put("data",null);
  9.         result.put("msg" , "算数异常:"+ e.getMessage());
  10.         return result;
  11.     }
  12. }
db252ef5ebac17748ded4eb8c4596fa3.png

如果再有一个空指针异常,那么上面的代码是不行的,还要写一个针对空指针异常处理器

  1. @ExceptionHandler(NullPointerException.class)
  2. public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {
  3.     HashMap<String, Object> result = new HashMap<>();
  4.     result.put("state",-1);
  5.     result.put("data",null);
  6.     result.put("msg" , "空指针异常异常:"+ e.getMessage());
  7.     return result;
  8. }
  9. @RequestMapping("/index")
  10. public String index(HttpServletRequest request,String username, String password) {
  11.     Object obj = null;
  12.     System.out.println(obj.hashCode());
  13.     return "Hello Index";
  14. }
a9597c922d930bf0e690edcb2620779b.png

但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

  1. @ExceptionHandler(Exception.class)
  2. public HashMap<String,Object> exceptionAdvice(Exception e) {
  3.     HashMap<String, Object> result = new HashMap<>();
  4.     result.put("state",-1);
  5.     result.put("data",null);
  6.     result.put("msg" , "异常:"+ e.getMessage());
  7.     return result;
  8. }

可以看到优先匹配的还是前面写的 空指针异常

231a210a1f971f4e7d8b0e61cac842e2.png

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

3. 统一数据格式返回

3.1 统一数据格式返回的实现

1.给当前类添加 @ControllerAdvice

2.实现 ResponseBodyAdvice 重写其方法

  • supports 方法,此方法表示内容是否需要重写(通过此⽅法可以选择性部分控制器和方法进行重写),如果要重写返回 true

  • beforeBodyWrite 方法,方法返回之前调用此方法

  1. @ControllerAdvice
  2. public class MyResponseAdvice implements ResponseBodyAdvice {
  3.     // 返回一个 boolean 值,true 表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法
  4.     // 返回 false 表示对结果不进行任何处理,直接返回
  5.     @Override
  6.     public boolean supports(MethodParameter returnType, Class converterType) {
  7.         return true;
  8.     }
  9.     // 方法返回之前调用此方法
  10.     @Override
  11.     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
  12.         HashMap<String,Object> result = new HashMap<>();
  13.         result.put("state",1);
  14.         result.put("data",body);
  15.         result.put("msg","");
  16.         return result;
  17.     }
  18. }
  19. @RestController
  20. @RequestMapping("/user")
  21. public class UserController {
  22.     @RequestMapping("/login")
  23.     public boolean login(HttpServletRequest request,String username, String password) {
  24.         boolean result = false;
  25.         if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
  26.             if(username.equals("admin") && password.equals("admin")) {
  27.                 HttpSession session = request.getSession();
  28.                 session.setAttribute("userinfo","userinfo");
  29.                 return true;
  30.             }
  31.         }
  32.         return result;
  33.     }
  34.     @RequestMapping("/reg")
  35.     public int reg() {
  36.         return 1;
  37.     }
  38. }
db9d23fb9a31ac6eb5beef582e559a11.png

3.2 @ControllerAdvice 源码分析

通过对 @ControllerAdvice 源码的分析我们可以知道上面统一异常和统一数据返回的执行流程

(1)先看 @ControllerAdvice 源码

e126d1a9fdac09cde491e1127316aecc.png

可以看到 @ControllerAdvice 派生于 @Component 组件而所有组件初始化都会调用 InitializingBean 接口

(2)下面查看 initializingBean 有哪些实现类

在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法

1c826f54aaa271b533f829f2f859744e.png

(3)而这个方法中有一个 initControllerAdviceCache 方法,查询此方法

442b20c707b1d5dcf3b3f77304378a4e.png

发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

23993da546316f475d96e7608b97a645.png

已在知识星球更新源码解析如下:

fa10e811cb23e7f3b8c6603fbb2e115d.jpeg

a6a94c96538f254cac1a20b99e858f7c.jpeg

281887eb4824685cd78e206ff476c5f6.jpeg

d39fb861ec92126adbf6516027541a8b.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

  1. 文章有帮助的话,在看,转发吧。
  2. 谢谢支持哟 (*^__^*)
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/71725
推荐阅读
相关标签
  

闽ICP备14008679号