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

news2024/12/26 17:21:39

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

  1. 用户登录权限的校验实现接口 HandlerInterceptor + WebMvcConfigurer
  2. 异常处理使用注解 @RestControllerAdvice + @ExceptionHandler
  3. 数据格式返回使用注解 @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: 自定义用户登录的拦截器
 * @Date 2023/2/13 13:06
 */
@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: 将自定义拦截器添加到系统配置中,并设置拦截的规则
 * @Date 2023/2/13 13:13
 */
@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.coloradmin.cn/o/786693.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于STM32CubeMX和keil采用STM32F407的基本定时器中断实现LED闪烁

文章目录 前言1. 电路原理图理解2. 基本定时器2.1 STM32定时器中断的流程&#xff1a;2.2 部分参数详解2.2.1 时钟源2.2.2 预分频系数2.2.3 自动重装载值 3. STM32CubeMX参数配置3.1GPIO配置3.2 时钟配置3.2 配置定时器相关参数3.3 Debug配置3.4 中断配置3.5 代码生成 4. keil代…

【每日一题】—— B. Ternary String (Educational Codeforces Round 87 (Rated for Div. 2))

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

ftp传文件越来越慢的原因,以及解决方案

FTP 是一种常用的文件传输协议&#xff0c;它基于客户端-服务端模型工作&#xff0c;允许用户通过网络传输文件。但是&#xff0c;有时候在使用 FTP 的过程中&#xff0c;文件传输速度会逐渐变慢&#xff0c;这给用户带来了很多困扰。本文将分析 FTP 传文件变慢的原因&#xff…

Jwt(Json web token)——从Http协议到session+cookie到Token Jwt介绍 Jwt的应用:登陆验证的流程

目录 引出从Http协议到session&cookie到TokenHTTP协议session & cookiesessioncookie为什么需要session & cookie? JavaEE传统解决长连接方案问题&#xff1a;分布式不适用解决方案&#xff1a;令牌Token Jwt&#xff0c;Json web tokenjwt的结构Header加密算法Ba…

MySQL Workbench的使用

MySQL Workbench 是一款专门为 MySQL 设计的可视化数据库管理软件&#xff0c;我们可以在自己的计算机上&#xff0c;使用图形化界面远程管理 MySQL 数据库。 MySQL Workbench 的初始界面如下图所示。 点击方框后会进入这个界面&#xff0c;这样就与数据库连接完毕了 使用 Wo…

Docker 全栈体系(四)

Docker 体系&#xff08;高级篇&#xff09; 一、Docker复杂安装 1. 安装mysql主从复制 主从搭建步骤 新建主服务器容器实例3307 docker run -p 3307:3306 --name mysql-master \ -v /mydata/mysql-master/log:/var/log/mysql \ -v /mydata/mysql-master/data:/var/lib/mysq…

SpringBoot-4

Spring Boot 使用 slf4j 日志 在开发中经常使用 System.out.println()来打印一些信息&#xff0c;但是这样不好&#xff0c;因为大量的使用 System.out 会增加资源的消耗。实际项目中使用的是 slf4j 的 logback 来输出日志&#xff0c;效率挺高的&#xff0c;Spring Boot 提供…

【go语言学习笔记】02 Go语言高效并发

文章目录 一、并发基础1. 协程&#xff08;Goroutine&#xff09;2. Channel2.1 声明2.2 无缓冲 channel2.3 有缓冲 channel2.4 关闭 channel2.5 单向 channel2.6 selectchannel 示例 二、同步原语1. 资源竞争2. 同步原语2.1 sync.Mutex2.2 sync.RWMutex2.3 sync.WaitGroup2.4 …

2023 NVIDIA 创乐博 CUDA 线上训练营笔记

一、学习ubuntu 2.1修改权限 linux指令学习 cd course //进入course目录 ls //列出文件夹 clean //清屏幕//---修改权限 chmod gow text //给text文件夹添加可写权限 chmod gw make.ip chmod 755 text 可读可写可执行&#xff08;user goup o…

MySQL MHA高可用配置及故障切换

1&#xff0e;什么是 MHA MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的…

uni-app中uni-table的uni-tr无点击事件

uni-app中uni-table的uni-tr无点击事件 问题描述解决方法一解决方法二解决方法三 问题描述 本文记录用于记录使用uni-app开发过程遇到的bug。 在使用uni-table时&#xff0c;想给uni-table的行添加行点击事件&#xff1b;但发现官方并未给uni-tr增加点击行点击事件&#xff08;…

gitee 配置ssh 公钥(私钥)

步骤1&#xff1a;添加/生成SSH公钥&#xff0c;码云提供了基于SSH协议的Git服务&#xff0c;在使用SSH协议访问项目仓库之前&#xff0c;需要先配置好账户/项目的SSH公钥。 绑定账户邮箱&#xff1a; git config --global user.name "Your Name" git config --glob…

Linux静态库+demo

1.什么是Linux静态库呢&#xff1f; Linux静态库&#xff08;Static Library&#xff09;是一种包含已编译的目标代码的文件集合&#xff0c;用于在链接时与其他目标代码一起组成可执行文件。与动态库&#xff08;Dynamic Library&#xff09;不同&#xff0c;静态库的代码在编…

集成学习Boosting - AdaBoost

目录 1. Boosting方法的基本思想 1.1 Bagging VS Boosting 1.2 Boosting算法的基本元素与基本流程 1.3 sklearn中的Boosting算法 2. AdaBoost 3 AdaBoost的基本参数与损失函数 3.1 参数 base_estimator&#xff0c;属性base_estimator_与estimators_ 3.1. 参数 learnin…

构建高效供应商管理体系,提升企业采购能力

随着企业采购规模的不断扩大和全球化竞争的加剧&#xff0c;供应商管理变得越来越重要。构建一个高效的供应商管理体系是企业提升采购能力、降低采购成本的关键一环。本文将重点探讨供应商管理体系的意义和作用&#xff0c;并介绍如何构建一个高效的供应商管理体系。 一、供应商…

Openlayers实战,Openlayers一个车辆转向运动轨迹动画,支持根据轨迹运动方向自动改变车头转向角度,无需定时器,丝滑小车转向运动效果

专栏目录: OpenLayers入门教程汇总目录 前言 本章作为OpenLayers入门文章《OpenLayers入门,OpenLayers动画效果实现,OpenLayers实现轨迹运动动画》的的增强进阶篇章,实现了入门教程中没有实现的小车车头方向根据运动方向自动转向的功能。 二、依赖和使用 "ol&quo…

四、运算符(1)

本章概要 开始使用优先级赋值 方法调用中的别名现象 算术运算符 一元加减运算符 递增和递减 Java 是从 C 的基础上做了一些改进和简化发展而成的。对于 C/C 程序员来说&#xff0c;Java 的运算符并不陌生。如果你已了解 C 或 C&#xff0c;大可以跳过本章和下一章&#xff0c…

微信小程序开店的步骤

越来越多实体店家在寻求入局电商的机会&#xff0c;随着微信推出了小程序&#xff0c;商家们看到了机会&#xff0c;微信小程序开发不仅成本较低&#xff0c;还能借助微信进行推广&#xff0c;帮助商家实现线上线下的对接&#xff0c;是商家实现开店的极佳选择。那么微信小程序…

Linux 网络收包流程

哈喽大家好&#xff0c;我是咸鱼 我们在跟别人网上聊天的时候&#xff0c;有没有想过你发送的信息是怎么传到对方的电脑上的 又或者我们在上网冲浪的时候&#xff0c;有没有想过 HTML 页面是怎么显示在我们的电脑屏幕上的 无论是我们跟别人聊天还是上网冲浪&#xff0c;其实…