springboot统一功能处理(用户登录权限效验-拦截器异常处理数据格式返回)(代码片段)

快到锅里来呀 快到锅里来呀     2023-03-07     653

关键词:

文章目录


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

  • 统一用户登录权限的效验实现接口 HandlerInterceptor + WebMvcConfigurer
  • 统一异常处理使用注解 @RestControllerAdvice + @ExceptionHandler
  • 统一数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice

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;
        
    

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

  1. 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断
  2. 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功
  3. 这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 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) 给当前的类添加 @Configuration 注解

    2)实现 WebMvcConfigurer 接口

    3)重写 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 练习:登录拦截器

要求

  1. 登录、注册页面不拦截,其他页面都拦截
  2. 当登录成功写入 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 之前进⾏相应的业务处理,执⾏的流程如下图所示

实现原理源码分析

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

  1. 而所有方法都会执行 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. 统一异常处理

  1. 给当前的类上加 @ControllerAdvice 表示控制器通知类
  2. 给方法上添加 @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 方法实现的

springboot统一功能处理(代码片段)

前言接下来是SpringBoot统⼀功能处理模块了,也是AOP的实战环节,要实现的课程⽬标有以下3个:统⼀⽤户登录权限验证统⼀数据格式返回统⼀异常处理接下我们⼀个⼀个来看。一、用户登录权限效验⽤户登录权限的发... 查看详情

springboot+shiro权限注解请求乱码解决统一异常处理

springboot+shiro权限注解、请求乱码解决、统一异常处理前篇后台权限管理系统相关:springboot+mybatis+layui+shiro后台权限管理系统springboot+shiro之登录人数限制、登录判断重定向、session时间设置springboot+shiro动态更新用户信息基于前篇... 查看详情

springboot拦截器使用和常用功能统一封装(代码片段)

文章目录1.拦截器1.1拦截器的使用1.2拦截器的原理2.用户登录权限校验3.统一异常处理4.统一数据返回格式1.拦截器1.1拦截器的使用Spring中提供了拦截器HandlerInteceptor,它的具体使用分为以下两个步骤:创建自定义拦截器ÿ... 查看详情

springboot实战——个人博客项目(代码片段)

...新增发表博客修改、删除自己的博客项目技术栈SSM(SpringBoot& 查看详情

springboot实例之用户登录

参考技术A上两篇完成了用户信息表的增删查,接下来增加用户登录功能。采用springsecurity来进行权限控制。我们希望用户可以通过用户名+密码、邮箱+密码、手机号+验证码、微信登录三种登录途径。先用来完成用户名+密码或手... 查看详情

iuap--单点登录

登录组件:提供统一的登录组件身份、证明验证身份支持多种身份标识,用户名、邮箱、手机号支持多个域,从与得到用户响应的角色,权限进行验证用户时候能进行操作。支持会话管理和安全管理支持多种验证策略,并支持扩... 查看详情

单点登录系统使用springsecurity的权限功能(代码片段)

...限功能目前配置中心已经包含了单点登录功能,可以通过统一页面进行登录,登录完会将用户写入用户表RBAC的用户、角色、权限表CRUD、授权等都已经完成希望不用用户再次登录,就可以使用SpringSecurity的权限控制SpringSecuritySpring... 查看详情

权限认证系统,实现类似单点登录的功能

用于统一管理所有集采相关站点的token,实现类似单点登录的功能。主要用于多服务集群,各子系统单独登录,想要实现单点登录的功能。首先新建用户认证表(总表),包括子系统所有的用户,要求token唯一性和随机性。用户... 查看详情

idm统一权限功能修改心得

...用户身份信息,保证系统安全,提高工作效率。统一权限是对各个业务系统的功能资源进行管控,权限资源授权体系包括组织、标准角色、实际角色、用户。目前可以通过标准角色管理用户、组织关联标准角色后会生... 查看详情

springboot中基于拦截器实现登录验证功能(代码片段)

...息等,只要是多个处理器都需要的即可使用拦截器实现。SpringBoot提供了Interceptor拦截器机制,用于请求的预处理和后处理。在SpringBoot中定义一个拦截器有两种方法:第一种是实现HandlerInterceptor接口,或者继承实现了HandlerIntercepto... 查看详情

springboot+shiro之登录人数限制登录判断重定向session时间设置

springboot+shiro之登录人数控制项目前篇:springboot+mybatis+layui+shiro后台权限管理系统本文是基于springboot+mybatis+layui+shiro后台权限管理系统开发的,新增功能:shiro并发登陆人数控制(超出登录用户最大配置数量,清理用户)功能;解决... 查看详情

基于springboot+mybatis的前后端分离实现在线办公系统(代码片段)

在线办公系统目录在线办公系统1.开发环境的搭建及项目介绍2.登录模块及配置框架搭建<1>Jwt工具类及对Token的处理1.1根据用户信息生成Token1.2根据Token生成用户名1.3判断Token是否有效1.4判断Token是否可以被刷新1.5刷新Token,... 查看详情

shiro简介

...录,授权认证的处理开发框架,它可以对所有的项目进行统一的授权处理操作。        查看详情

springboot使用validator进行参数校验(代码片段)

Springboot使用validator进行参数校验添加数据效验单字段效验级联效验分组效验与组序列多字段关联效验执行数据效验@Valid注解处理validator异常controller接口处理ValidatorUtils工具类自定义异常及处理ValidatorUtils与接口添加数据效验&l... 查看详情

springboot使用validator进行参数校验(代码片段)

Springboot使用validator进行参数校验添加数据效验单字段效验级联效验分组效验与组序列多字段关联效验执行数据效验@Valid注解处理validator异常controller接口处理ValidatorUtils工具类自定义异常及处理ValidatorUtils与接口添加数据效验&l... 查看详情

springboot使用validator进行参数校验

Springboot使用validator进行参数校验添加数据效验单字段效验级联效验分组效验与组序列多字段关联效验执行数据效验@Valid注解处理validator异常controller接口处理ValidatorUtils工具类自定义异常及处理ValidatorUtils与接口添加数据效验&l... 查看详情

springboot+vue博客项目总结(代码片段)

文章目录Springboot+Vue博客项目总结1.工程搭建1.1新建maven工程1.2application.properties配置1.3配置分页插件1.4配置解决跨域1.5添加启动类2.统一异常处理3.登录功能实现3.1接口说明3.2JWT3.3Controller3.4Service3.5登录参数,redis配置5.获取... 查看详情

springboot使用validator进行参数校验(代码片段)

Springboot使用validator进行参数校验添加数据效验单字段效验级联效验分组效验与组序列多字段关联效验执行数据效验@Valid注解处理validator异常controller接口处理ValidatorUtils工具类自定义异常及处理ValidatorUtils与接口添加数据效验&l... 查看详情