Spring Boot 统一功能处理(拦截器实现用户登录权限的统一校验、统一异常返回、统一数据格式返回)

news2024/11/25 0:46:28

目录

1. 用户登录权限校验

1.1 最初用户登录权限效验

1.2 Spring AOP 用户统⼀登录验证

1.3 Spring 拦截器

(1)创建自定义拦截器

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

1.4 练习:登录拦截器

(1)实现 UserController 实体类

(2)返回的登录页面:login.html

(3)实现效果

 1.5 拦截器实现原理

(1)实现原理源码分析

1.6 统一访问前缀添加

(1)在系统的配置文件中设置

 (2)在 application.properies 中配置

2. 统一的异常处理

2.1 异常的统一封装

(1)创建一个类,并在类上标识:@ControllerAdvice

 (2)添加方法 @ExceptionHandler 来订阅异常

3. 统一数据返回格式

3.1 为什么要统一数据返回格式

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

(1)创建一个类,并添加 @ControllerAdvice

(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法

4. @ControllerAdvice 源码分析

(1) @ControllerAdvice 源码

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

 (3)查询 initControllerAdviceCache 方法


本节主要讲解Spring Boot 统一功能处理,同样也是 AOP 的实战环节,我们希望能够实现以下目标:

  1. 统一用户登陆权限验证
  2. 统一异常处理
  3. 统一数据格式返回

1. 用户登录权限校验

回顾一下最初用户登录验证的实现方法:

  1. 最初的用户登录校验版本:在每个方法中获取 Session 以及 Session 中的信息,对用户账号以及密码进行校验,正确则登录成功,反之则失败
  2. 第二版本:实现统一方法去校验是否登陆成功,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断
  3. 第三版本:使用 Spring AOP 来进行用户统一登录校验
  4. 第四版本:使用 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 的切面中实现用户登录权限效验的功能,有这样两个问题:

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

1.3 Spring 拦截器

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

  1.     创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法
  2.     将自定义拦截器加入到框架的配置中,并且设置拦截规则

(1)创建自定义拦截器

//实现 HandlerInterceptor 接口
public class loginInterceptor implements HandlerInterceptor {
    /**
     * 返回 true 继续下序流程
     * false 表示验证失败
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 用户登录业务判断
        // false 表示当不存在 session 不存在时不需要创造一个会话信息
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null){
            // 说明用户已经登录
            return true;
        }
        // 可以直接跳转到登录页面 或 返回一个 401、403 没有权限码
        response.sendRedirect("/login.html");
//        response.setStatus(401);
        return false;
    }
}

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

  • addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法
  • excludePathPatterns:表示需要排除的 URL
@Configuration // 让随着spring启动而启动
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new loginInterceptor())
                .addPathPatterns("/**")// 拦截所有请求
                .excludePathPatterns("/user/login")// 不拦截的 url 地址
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/**/*.html");
    }
}

1.4 练习:登录拦截器

实现愿望:

  1. 登录、注册页面不拦截,其余页面都拦截
  2. 等登陆成功写入 session 后,拦截页面可访问

(1)实现 UserController 实体类


@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/getUser")
    public String getuser(){
        System.out.println("执行了 getUser !");
        return "get user";
    }

    @RequestMapping("/login")
    public String login(){
        System.out.println("执行了 login !");
        return "get login";
    }

    @RequestMapping("/reg")
    public String reg(){
        System.out.println("执行了 reg !");
        return "get reg";
    }
}

(2)返回的登录页面:login.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>

(3)实现效果

 

 1.5 拦截器实现原理

 

(1)实现原理源码分析

  1. 所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现 
  2. 而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码分析如下:

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

1.6 统一访问前缀添加

方法:

  1. 在系统的配置文件中设置
  2. 在 application.properies 中配置

(1)在系统的配置文件中设置

/**
 * 所有的接口添加 api 前缀
 * c 代表所有的请求(Controller)
 * 表示所有的地址都会加上这个前缀
 * @param configurer
 */
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    configurer.addPathPrefix("api",c -> true);
}

现在我们去查看之前不被拦截的地址

 (2)在 application.properies 中配置

 

2. 统一的异常处理

  1. 给当前的类上加 @ControllerAdvice 表示控制器通知类
  2. 给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码

我们先去制造些异常:

 

2.1 异常的统一封装

(1)创建一个类,并在类上标识:@ControllerAdvice

@ControllerAdvice
public class ExceptionHandler {
}

 (2)添加方法 @ExceptionHandler 来订阅异常

@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {

    /**
     * 拦截所有的空指针异常,继续统一的数据返回
     */

    @ExceptionHandler(NullPointerException.class)// 空指针异常
    public HashMap<String,Object> nullException(NullPointerException e){
        HashMap<String,Object> result = new HashMap<>();
        result.put("code","-1");
        result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息
        result.put("date",null);
        return result;
    }

}

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

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

@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {

    /**
     * 拦截所有的空指针异常,继续统一的数据返回
     */

    @ExceptionHandler(NullPointerException.class)// 空指针异常
    public HashMap<String,Object> nullException(NullPointerException e){
        HashMap<String,Object> result = new HashMap<>();
        result.put("code","-1");
        result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息
        result.put("date",null);
        return result;
    }

    @ExceptionHandler(Exception.class)// 所有异常
    public HashMap<String,Object> AllException(NullPointerException e){
        HashMap<String,Object> result = new HashMap<>();
        result.put("code","-1");
        result.put("msg","异常:" + e.getMessage());//错误码的描述信息
        result.put("date",null);
        return result;
    }
}

 

 

3. 统一数据返回格式

3.1 为什么要统一数据返回格式

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回的
  3. 有利于项目统一数据的维护和修改。
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。

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

(1)创建一个类,并添加 @ControllerAdvice

@ControllerAdvice
public class ResponseAdvice {

}

(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    /**
     * 表示是否需要重写
     * 返回true则执行beforeBodyWrite方法,反之则不执行
     */
    @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> hashMap = new HashMap<>();
        hashMap.put("code",200);// 状态码
        hashMap.put("msg","");// 错误的描述信息
        hashMap.put("date",body);
        return hashMap;
    }
}

supports方法相当于是一个开关,只有当 true 时才能执行重写 beforeBodyWrite 方法,false就不重写

当访问 getUser 时发生异常了,类型访问异常 

注意:

我们知道String既不属于基本数据类型,又不属于对象且在重写方法的时候其余类型都是用的统一的格式化工具,而String用的是它自身的格式化工具,String自身的格式化工具在执行的时候还没有加载好,就会导致 原始类型 是String的时候,在转化成HashMap的时候就会报错

所以在统一返回的时候需要对String进行单独的处理

 

jackson就是用于 json 数据转换的,json的转换工具 

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    HashMap<String,Object> hashMap = new HashMap<>();
    hashMap.put("code",200);// 状态码
    hashMap.put("msg","");// 错误的描述信息
    hashMap.put("date",body);
    if (body instanceof String){
        // 判断数据类型是不是 String,是String需要特殊处理,因为 String 在转换的时候会报错
        try {
            return objectMapper.writeValueAsString(hashMap);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
    return hashMap;
}

4. @ControllerAdvice 源码分析

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

(1) @ControllerAdvice 源码

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

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

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

 (3)查询 initControllerAdviceCache 方法

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

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

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

相关文章

app测试和web测试的区别是什么?【软件测试经典面试题】

单纯从功能测试的层面上来讲的话&#xff1a; APP 测试、web 测试 在流程和功能测试上是没有区别的。 1.系统架构方面&#xff1a; web项目&#xff0c;一般都是b/s架构&#xff0c;基于浏览器的 app项目&#xff0c;则是c/s的&#xff0c;必须要有客户端&#xff0c;用户需…

AI绘画(1)stable diffusion安装教程

1、引言 stable diffusion 是一款免费开源的AI绘画工具&#xff0c;它能够帮助任何人轻松地进行绘画创作。不论你是有绘画基础还是完全没有经验&#xff0c;stable diffusion 都能让你在数字画布上释放创造力。 stable diffusion 提供了丰富多样的绘画工具和选项&#xff0c;…

公会在tiktok发展,有哪些国家,怎么入驻呢?

在秀场直播领域&#xff0c;众多公会都将目光聚焦在TikTok上。TikTok已成为一个新的金矿&#xff0c;许多公会已在这个平台上赚得盆满钵满。 这些公会在TikTok上月流水达到数百万美元&#xff0c;甚至在一场PK中流水达到40万美元&#xff0c;分成比例高达80%。TikTok的秀场直播…

PostGIS 矢量瓦片

title: PostGIS 矢量瓦片 date: 2023-08-07 author: ac tags: vector tile categories:Database Martin - 基于PostGIS的矢量瓦片服务器 1. 简介 目前流行的矢量瓦片的切图方案&#xff1a; mapbox gl tippecanoe &#xff1a;v2收费&#xff0c;tippecanoe是mapbox官方推…

【C语言】结构体详解

现实生活中一个事物&#xff0c;会有许多属性连接起来。而C语言引入一种构造数据类型——结构体 将属于一个事物的多个数据组织起来以体现其内部联系。 一、结构体类型的定义 结构体类型 是一种 构造类型&#xff0c;它是由若干成员组成的&#xff0c;每个成员可以是一个基本…

python:使用geopandas和rasterio将矢量范围内的栅格值赋为0并重新输出

需求&#xff1a;有一个点shp文件和一个栅格&#xff0c;想要构建shp中每个点的缓冲区&#xff0c;并且缓冲区范围内的栅格值重新赋为0并输出新的tif文件 解决方法&#xff1a;使用python中的geopandas和rasterio中的掩膜操作实现 代码如下&#xff1a; import numpy as np …

数据结构(一):顺序表详解

在正式介绍顺序表之前&#xff0c;我们有必要先了解一个名词&#xff1a;线性表。 线性表&#xff1a; 线性表是&#xff0c;具有n个相同特性的数据元素的有限序列。常见的线性表&#xff1a;顺序表、链表、栈、队列、数组、字符串... 线性表在逻辑上是线性结构&#xff0c;但…

【验证码逆向专栏】最新某度旋转验证码 v2 逆向分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;不提供完整代码&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 本文章未…

使用requests如何实现自动登录

不知道大家有没有注意到&#xff0c;好多网站我们登录过后&#xff0c;在之后的某段时间内访问该网页时&#xff0c;不会给出请登录的提示&#xff0c;时间到期后就会提示请登录&#xff01;这样在使用爬虫访问网页时还要登录&#xff0c;打乱我们的节奏&#xff0c;那么如何使…

ISC 2023 大会成功举办,向量数据库公司 Zilliz 成大模型论坛焦点

近日,第十一届互联网安全大会(ISC 2023)在北京盛大开幕。大会由 ISC 互联网安全大会组委会、中国互联网协会、中国网络空间安全协会、全国工商联大数据运维(网络安全)委员会、中国人工智能学会、中国软件行业协会、中国企业联合会、360 互联网安全中心主办;中国通信企业协…

使用阿里云服务器部署和使用GitLab

本文阿里云百科分享使用阿里云服务器部署和使用GitLab&#xff0c;GitLab是Ruby开发的自托管的Git项目仓库&#xff0c;可通过Web界面访问公开的或者私人的项目。本教程介绍如何部署和使用GitLab。 目录 准备工作 部署GitLab环境 使用GitLab 登录GitLab 生成密钥对文件并…

HoloLens 2设备MR 应用交互设计

AR 眼镜实现了虚拟世界与现实世界的融合&#xff0c;完成屏幕的“跨越”&#xff0c;人机交互设计也从二维平面迈向三维世界。目前&#xff0c;MR 应用的人机交互界面仍然处于早期发展阶段&#xff0c;各种理念和方法仍处于逐步形成与应用阶段&#xff0c;低成本地完成使用者从…

记录一下Java实体转json字段顺序问题

特殊需求&#xff0c;和C交互他们那边要求字段顺序要和他们定义的一致(批框架) 如下&#xff1a; Data public class UserDto {private String name;private Integer age;private String addr; }未转换前打印&#xff1a; 转换后打印&#xff1a; 可以看到转换为json顺序打印…

第9届Python编程挑战赛北京赛区复赛真题剖析-2023年全国青少年信息素养大赛

[导读]&#xff1a;超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲&#xff0c;这是超平老师解读Python编程挑战赛系列的第16讲。 全国青少年信息素养大赛&#xff08;原全国青少年电子信息智能创新大赛&#xff09;是“世界机器人大会青少年机器人设计与…

Spring与Spring Bean

Spring 原理 它是一个全面的、企业应用开发一站式的解决方案&#xff0c;贯穿表现层、业务层、持久层。但是 Spring 仍然可 以和其他的框架无缝整合。 Spring 特点 轻量级 控制反转 面向切面 容器 框架集合 Spring 核心组件 Spring 总共有十几个组件核心容器(Spring core) S…

02.Deep Visual-Semantic Alignments for Generating Image Descriptions

目录 前言泛读摘要IntroductionRelated Work小结 精读Model3.1 学习对齐视觉与语言数据图片表征句子表征对齐目标损失函数解码文本片段对齐图像 MRNN生成描述优化 实验结论 代码 前言 本课程来自深度之眼《多模态》训练营&#xff0c;部分截图来自课程视频。 文章标题&#xf…

湘大 XTU OJ 1290 Alice and Bob 题解(非常详细):字符串 分类讨论 简单模拟

一、链接 1290 Alice and Bob 二、题目 题目描述 Alice和Bob玩剪刀-石头-布的游戏&#xff0c;请你写个程序判断一下比赛的结果。 输入 第一行是一个整数K&#xff0c;表示样例的个数。 以后每行两个单词&#xff0c;rock表示石头&#xff0c;paper表示布&#xff0c;scis…

ETL技术入门之ETLCloud初认识

首先ETL是什么&#xff1f; ETL代表“Extract, Transform, Load”&#xff0c;是一种用于数据集成和转换的过程。它在数据管理和分析中扮演着重要的角色。下面我们将分解每个步骤&#xff1a; Extract&#xff08;抽取&#xff09;&#xff1a; 这一步骤涉及从多个不同的数据源…

小说推文怎么做详细教程。小说推文项目拆解及分享

科思创业汇 大家好&#xff0c;这里是科思创业汇&#xff0c;一个轻资产创业孵化平台。赚钱的方式有很多种&#xff0c;我希望在科思创业汇能够给你带来最快乐的那一种&#xff01; 如何制作小说推文的详细教程&#xff01; 我做自媒体已经五年了&#xff0c;在自媒体行业也…

YOLOv5、YOLOv8改进:MobileViT:轻量通用且适合移动端的视觉Transformer

MobileViT: Light-weight, General-purpose, and Mobile-friendly Vision Transformer 论文&#xff1a;https://arxiv.org/abs/2110.02178 1简介 MobileviT是一个用于移动设备的轻量级通用可视化Transformer&#xff0c;据作者介绍&#xff0c;这是第一次基于轻量级CNN网络性…