这才是 SpringBoot 统一登录鉴权、异常处理、数据格式的正确打开姿势

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

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

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

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

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

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

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

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

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

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

 1.1 最初用户登录权限效验

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/a1")
    public Boolean login (HttpServletRequest request) {
        // 有 Session 就获取,没有就不创建
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            // 说明已经登录,进行业务处理
            return true;
        } else {
            // 未登录
            return false;
        }
    }

    @RequestMapping("/a2")
    public Boolean login2 (HttpServletRequest request) {
        // 有 Session 就获取,没有就不创建
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            // 说明已经登录,进行业务处理
            return true;
        } else {
            // 未登录
            return false;
        }
    }
}

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

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

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

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

 1.2 Spring AOP 统一用户登录验证

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

@Aspect // 当前类是一个切面
@Component
public class UserAspect {
    // 定义切点方法 Controller 包下、子孙包下所有类的所有方法
    @Pointcut("execution(* com.example.springaop.controller..*.*(..))")
    public void  pointcut(){}
    
    // 前置通知
    @Before("pointcut()")
    public void doBefore() {}
    
    // 环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        System.out.println("Around 方法开始执行");
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("Around 方法结束执行");
        return obj;
    }
}

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

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

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

1.3 Spring 拦截器 

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

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

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

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

  • 实现 WebMvcConfigurer 接口

  • 重写 addInterceptors 方法

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

(1)创建自定义拦截器

/**
 * @Description: 自定义用户登录的拦截器
 */
@Component
public class LoginIntercept implements HandlerInterceptor {
    // 返回 true 表示拦截判断通过,可以访问后面的接口
    // 返回 false 表示拦截未通过,直接返回结果给前端
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        // 1.得到 HttpSession 对象
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            // 表示已经登录
            return true;
        }
        // 执行到此代码表示未登录,未登录就跳转到登录页面
        response.sendRedirect("/login.html");
        return false;
    }
}

(2)将自定义拦截器添加到系统配置中,并设置拦截的规则

  • addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法

  • excludePathPatterns:表示需要排除的 URL

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

/**
 * @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则
 */
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Resource
    private LoginIntercept loginIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        registry.addInterceptor(new LoginIntercept());//可以直接new 也可以属性注入
        registry.addInterceptor(loginIntercept).
                addPathPatterns("/**").    // 拦截所有 url
                excludePathPatterns("/user/login"). //不拦截登录注册接口
                excludePathPatterns("/user/reg").
                excludePathPatterns("/login.html").
                excludePathPatterns("/reg.html").
                excludePathPatterns("/**/*.js").
                excludePathPatterns("/**/*.css").
                excludePathPatterns("/**/*.png").
                excludePathPatterns("/**/*.jpg");
    }
}

1.4 练习:登录拦截器

要求

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

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

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

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

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

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request,String username, String password) {
        boolean result = false;
        if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            if(username.equals("admin") && password.equals("admin")) {
                HttpSession session = request.getSession();
                session.setAttribute("userinfo","userinfo");
                return true;
            }
        }
        return result;
    }

    @RequestMapping("/index")
    public String index() {
        return "Hello Index";
    }
}

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

1.5 拦截器实现原理

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

实现原理源码分析

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

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

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

1.6 统一访问前缀添加

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

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 所有的接口添加 api 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

2. 统一异常处理

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

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

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/index")
    public String index() {
        int num = 10/0;
        return "Hello Index";
    }
}

在 config 包中,创建 MyExceptionAdvice 类

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

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

@ControllerAdvice
public class MyExceptionAdvice {
    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state",-1);
        result.put("data",null);
        result.put("msg" , "算数异常:"+ e.getMessage());
        return result;
    }
}

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

@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("state",-1);
    result.put("data",null);
    result.put("msg" , "空指针异常异常:"+ e.getMessage());
    return result;
}
@RequestMapping("/index")
public String index(HttpServletRequest request,String username, String password) {
    Object obj = null;
    System.out.println(obj.hashCode());
    return "Hello Index";
}

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

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

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

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

3. 统一数据格式返回

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

(1)给当前类添加 @ControllerAdvice

(2)实现 ResponseBodyAdvice 重写其方法

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

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

@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {

    // 返回一个 boolean 值,true 表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法
    // 返回 false 表示对结果不进行任何处理,直接返回
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    // 方法返回之前调用此方法
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String,Object> result = new HashMap<>();
        result.put("state",1);
        result.put("data",body);
        result.put("msg","");
        return result;
    }
}
@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request,String username, String password) {
        boolean result = false;
        if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            if(username.equals("admin") && password.equals("admin")) {
                HttpSession session = request.getSession();
                session.setAttribute("userinfo","userinfo");
                return true;
            }
        }
        return result;
    }

    @RequestMapping("/reg")
    public int reg() {
        return 1;
    }
}

 

3.2 @ControllerAdvice 源码分析 

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

(1)先看 @ControllerAdvice 源码

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/153471.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

阿尔法狗的算法解析-增强学习和蒙特卡洛树搜索算法

阿尔法狗(AlphaGo)是谷歌旗下DeepMind开发的一个著名的增强学习算法,它在围棋领域取得了显著的成就。本文主要探讨其中两个重要的算法:增强学习算法和蒙特卡洛树搜索算法。 AlphaGo涉及的算法 AlphaGo是DeepMind团队开发的一个由多种算法和技术组合而成的系统,其包括以下…

【Linux网络】典型NAS存储方式:NFS网络共享存储服务

一、关于存储的分类 二、NFS的介绍 nfs的相关介绍&#xff1a; 1、原理 2、nfs的特点 3、nfs软件学习 4、共享配置文件的书写格式 关于权限&#xff0c;学习&#xff1a; 5、关于命令的学习&#xff1a; 三、实验操作 1、nfs默认共享权限&#xff08;服务端设置&#…

大数据-之LibrA数据库系统告警处理(ALM-12049 网络读吞吐率超过阈值)

告警解释 系统每30秒周期性检测网络读吞吐率&#xff0c;并把实际吞吐率和阈值&#xff08;系统默认阈值80%&#xff09;进行比较&#xff0c;当检测到网络读吞吐率连续多次&#xff08;默认值为5&#xff09;超过阈值时产生该告警。 用户可通过“系统设置 > 阈值配置 >…

【数据结构】C语言实现栈

&#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;C/C领域新星创作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;数据结构与算法&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;大家…

软件开发和软件测试,到底学哪个好呢?

写在前面&#xff1a;买车没有最好&#xff0c;只有最适合。 类似这类“很难选择”的问题&#xff0c;在知乎上其实有很多。 比如&#xff1a;“该去年薪10w的国家电网&#xff0c;还是去年薪40w的互联网大厂”&#xff1b; 比如&#xff1a;“城里有房&#xff0c;剩下的100…

Sentinel规则

一、服务熔断测试 例子: application.properties配置文件 server.port8083spring.application.nameorder#spring.cloud.nacos.discovery.server-addrhttp://192.168.44.64:80spring.cloud.nacos.discovery.server-addrlocalhost:8848spring.cloud.sentinel.transport.port999…

C++之map和set模拟实现

前言 在map和set的使用文章中提到了CSTL中的map和set的底层其实就是用的红黑树来实现的,所以可以用红黑树来简单模拟实现一下STL中的map和set. STL源码中map和set的实现 map: 我们看到它的底层这个成员变量其实就是一棵红黑树, 之前说过map其实就对应搜索树的KV模型&#x…

面向企业的人脸属性检测技术方案

人脸识别技术已经成为企业提升服务质量、优化用户体验的重要工具。美摄科技&#xff0c;作为领先的人工智能技术提供商&#xff0c;我们致力于为企业提供最先进、最全面的人脸属性检测技术解决方案。 我们的AI人脸检测与属性分析技术&#xff0c;能够快速准确地检测人脸并返回…

安全狗云安全体系为高校提升立体化纵深防御能力

客户情况 某高校有服务器500台&#xff0c;对外站点200个&#xff0c;核心交换流量20G。 客户痛点 校园网系统分类较多&#xff0c;并且每类网站中安全级重要程度又各不相同&#xff0c;同时有多个网络出口(如&#xff1a;教育网、电信网、移动网等)&#xff0c;二级学院存在…

物联网网关在工业行业的应用案例

物联网网关在工业行业的应用案例 随着物联网技术的不断发展&#xff0c;物联网网关在工业行业的应用越来越广泛。本文将介绍一个物联网网关在工业行业的应用案例&#xff0c;以期为相关领域的研究和实践提供借鉴和启示。 一、案例背景 某大型制造企业是一家全球知名的汽车制…

vscode中git拉取、提交代码、解决冲突,以及合并代码的操作

vscode中git拉取、提交代码、解决冲突&#xff0c;以及合并代码的操作 场景&#xff1a;本地有修改代码&#xff0c;远程仓库没有更新&#xff0c;这时本地想要提交代码。 步骤&#xff1a;本地修改了testA文件内容->本地先暂存提交->拉取->推送&#xff1b; 本地修改…

2023.11.14 hivesql的容器,数组与映射

目录 https://blog.csdn.net/m0_49956154/article/details/134365327?spm1001.2014.3001.5501https://blog.csdn.net/m0_49956154/article/details/134365327?spm1001.2014.3001.5501 8.hive的复杂类型 9.array类型: 又叫数组类型,存储同类型的单数据的集合 10.struct类型…

SpringNative遇到的问题

问题1 org.apache.catalina.LifecycleException: An invalid Lifecycle transition was attempted ([before_stop]) for component 用不了反射&#xff0c;所以需要这个文件去 package org.wxy.example.sqlite.config;import java.lang.reflect.Constructor; import java.lan…

【机器学习基础】多元线性回归(适合初学者的保姆级文章)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;机器学习 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; 【机器学习基础】机器学习入门&#xff08;1&#xff09; 【机器学习基…

BI智能财务分析真的神,财务人都来用

不用等&#xff0c;真的不用等&#xff01;这边接入数据&#xff0c;那边就能把利润表、资产负债表、现金流量表等财务数据分析报表送到眼前&#xff0c;不用开发&#xff0c;直接就看分析结果。 奥威BI财务方案真能把我要的指标、分析都做出来&#xff1f; 能&#xff0c;可…

在win10环境下安装python,配置python环境,执行python脚本

1.安装python 去python官网下载&#xff1a; https://www.python.org/ 这里采用 Python 3.10.8 版本 选择windows 64位 双击安装&#xff1a; 安装这里有两个选项&#xff1a; 1.默认安装直接选Install Now 2.勾选install launcher for all users&#xff08;recommend&a…

Github小彩蛋显示自己的README,git 个人首页的 README,readme基本语法

先上效果&#x1f447; 代码在下面&#xff0c;流程我放最下面了&#xff0c;思路就是创建一个和自己同名的仓库&#xff0c;要公开&#xff0c;创建的时候会提示小彩蛋你的reademe会展示在你的首页&#xff0c;或许你在这个readme里面的修改都会在你的主页上看到了&#x1f44…

TEMU平台要求电子产品提供的UL测试报告如何办理?

平台销售的电子产品&#xff0c;要符合指定的标准&#xff0c;如果不合格很容易发生起火&#xff0c;等危及消费者生命财产的安全&#xff0c;因此很多客户因为缺少UL报告&#xff0c;导致产品被下架。 带电的产品上架亚马逊或相关的跨境电商平台都需要相关的UL报告/UL标准&…

vue-router配置

1、路由安装 npm install vue-router4 2、创建router目录 3、编辑文件且引入router包 4、main.js引入

申明式管理方式与配置清单文件

目录 申明式管理方式 1、使用申明式管理方式相关操作 1&#xff09;获取资源配置清单 2&#xff09;更改获取的yaml配置清单&#xff0c;并进行修改然后创建或更新资源 3&#xff09;在线修改或编辑资源配置 4&#xff09;删除资源 2、如何获取资源配置清单文件模板&…
最新文章