• SpringBoot 统一登录鉴权、异常处理、数据格式


    本篇将要学习 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. @Before("pointcut()")
    9. public void doBefore() {}
    10. // 环绕通知
    11. @Around("pointcut()")
    12. public Object doAround(ProceedingJoinPoint joinPoint) {
    13. Object obj = null;
    14. System.out.println("Around 方法开始执行");
    15. try {
    16. obj = joinPoint.proceed();
    17. } catch (Throwable e) {
    18. e.printStackTrace();
    19. }
    20. System.out.println("Around 方法结束执行");
    21. return obj;
    22. }
    23. }

    但如果只在以上代码 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

    (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)运行程序,访问页面,对比登录前和登录后的效果

     

    1.5 拦截器实现原理

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

     实现原理源码分析

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

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

     通过源码分析,可以看出,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. }

    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 arithmeticExceptionAdvice(ArithmeticException e) {
    5. HashMap 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 arithmeticExceptionAdvice(ArithmeticException e) {
    6. HashMap result = new HashMap<>();
    7. result.put("state",-1);
    8. result.put("data",null);
    9. result.put("msg" , "算数异常:"+ e.getMessage());
    10. return result;
    11. }
    12. }

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

    1. @ExceptionHandler(NullPointerException.class)
    2. public HashMap nullPointerExceptionAdvice(NullPointerException e) {
    3. HashMap 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. }

     

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

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

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

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

    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 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. }

          

    3.2 @ControllerAdvice 源码分析

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

    (1)先看 @ControllerAdvice 源码

     

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

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

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

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

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

  • 相关阅读:
    梭子鱼替换案例:国产防护甄选CACTER邮件安全网关
    VMware vCenter Server 7 升级
    计算机毕业设计Javaweb家庭财务管理系统(源码+系统+mysql数据库+lw文档)
    CSS逻辑组合伪类
    VsCode中C文件调用其他C文件函数失败
    nvidia-smi
    unity 射线检测,鼠标点击3D物体交互
    springboot使用MongoTemplate根据正则表达式查询日期数据
    js数据结构(队列Queue)
    并发编程-延时队列DelayQueue
  • 原文地址:https://blog.csdn.net/LU58542226/article/details/133105419