• SpringBoot项目自定义注解实现RBAC权限校验


    SpringBoot项目自定义注解实现RBAC权限校验

    之前的博客介绍了RBAC的原理,

    现在我们来介绍springboot如何基于RBAC自制简易的权限验证

    1、前言

    学过Spring Security的小伙伴都知道,SpringBoot项目可以集成Spring Security做权限校验框架,然后在Controller接口上直接使用@PreAuthorize注解来校验权限,但是如果我不想引入像Security、Shiro等第三方框架,也要实现权限校验的效果,该怎么做呢?

    接下来就给大家介绍一种方案:拦截器+自定义注解做基于RBAC模型的权限校验

    2、实现思路

    在这里插入图片描述

    这里做了一些简单的修改,

    比如redis存入的key是token,value是对应的user对象

    在登录成功之后,user对象里面有对应的list权限资源,从数据库中获取

    这里没有对应的权限资源表,所以使用aop切面注解进行权限比较的时候,使用固定的list比较

    正常应该通过账号,查询角色,通过角色查询对应的资源信息

    3、代码实现

    3.1、导入依赖

        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <version>1.16.12version>
            dependency>
            <dependency>
                <groupId>org.aspectjgroupId>
                <artifactId>aspectjrtartifactId>
                <version>1.8.9version>
            dependency>
            
            <dependency>
                <groupId>org.aspectjgroupId>
                <artifactId>aspectjtoolsartifactId>
                <version>1.8.9version>
            dependency>
            <dependency>
                <groupId>org.aspectjgroupId>
                <artifactId>aspectjweaverartifactId>
                <version>1.7.4version>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
            
            <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-allartifactId>
                <version>5.7.17version>
            dependency>
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>fastjsonartifactId>
                <version>1.2.62version>
            dependency>
            
            <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-allartifactId>
                <version>5.7.17version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    3.2、登录认证

    controller编写

    package com.melody.rest.restcontroller;
    
    import com.melody.rest.domain.RestSysUser;
    import com.melody.rest.model.ResultJson;
    import com.melody.rest.service.RestAuthService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    @RequestMapping("/rest")
    public class LoginController {
    
        @Autowired
        private RestAuthService restAuthService;
    
    
        //登录
        @PostMapping(value = "/login")
        public ResultJson index(@RequestBody RestSysUser restSysUser){
            //登录以及登录成功存入token
            return restAuthService.Login(restSysUser);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    service层实现

        @Override
        public ResultJson Login(RestSysUser restSysUser) {
            //账号密码校验,
            if("admin".equals(restSysUser.getUsername()) && "123456".equals(restSysUser.getPassword())){
                //账号密码正确
                //登录成功
                restSysUser.setResources(ResourceVerification.resource());
                Map<String, Object> userMap = BeanUtil.beanToMap(restSysUser, new HashMap<>(),
                        CopyOptions.create()
                                .setIgnoreNullValue(true)//忽略一些空值
                                .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
                UUID uuid = UUID.randomUUID();
                String tokenKey= String.valueOf(uuid);
                String token="LoginUserKey "+tokenKey;
                //存储
                redisTemplate.opsForHash().putAll(token,userMap);
                //设置存值时间,expire默认秒:1天
                redisTemplate.expire(token,60*60*24, TimeUnit.MINUTES);
                return ResultJson.ok(token);
            }else{
                //账号密码不正确
                return ResultJson.failure(ResultCode.LOGIN_ERROR);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    其中 restSysUser.setResources(ResourceVerification.resource());

    获取用户的权限资源:这里是写死了资源,正常是要从数据库获取

    package com.melody.rest.util;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Component
    public class ResourceVerification {
        //权限比较
        private List<String> resources = new ArrayList<>();
    
        public Boolean compareResource(String temp){
            initResources();
            if (resources.contains(temp)){
                return true;
            }
            return false;
        }
        public void initResources(){
            //模拟用户权限
            resources.add("/rest/test1");
            resources.add("/rest/test2");
            resources.add("/rest/test3");
            resources.add("/rest/test4");
        }
    
        //模拟实现从数据库获取资源
        public static List<String> resource(){
            List<String> resourcesUser = new ArrayList<>();
            resourcesUser.add("/testRest/t1");
            resourcesUser.add("/testRest/t2");
            resourcesUser.add("/testRest/t3");
            resourcesUser.add("/testRest/t4");
    
            return resourcesUser;
        }
    
        //是否包含该权限
        public Boolean compareResourceRest(List<String> resources,String auth){
            if (auth.length()!=0) {
               if(resources.contains(auth)){
                   return true;
               }
            }
            return false;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    3.3、配置拦截器

    这个拦截器拦截所有的请求,是查看有没有token以及该token绑定的用户账号是否正常,如果没有token则直接提示没有登录禁止访问。

    MvcConfig(拦截器配置)

    package com.melody.rest.config;
    
    import com.melody.rest.util.LoginInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    
    import javax.annotation.Resource;
    
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        RedisTemplate redisTemplate;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
    
            //配置登录查看是否有token拦截器
            registry.addInterceptor(new LoginInterceptor(redisTemplate)).addPathPatterns("/testRest/**").order(0);
    
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    LoginInterceptor

    package com.melody.rest.util;
    
    
    import cn.hutool.core.bean.BeanUtil;
    import com.alibaba.fastjson.JSONObject;
    import com.melody.rest.domain.RestData;
    import com.melody.rest.domain.RestSysUser;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Map;
    
    public class LoginInterceptor implements HandlerInterceptor {
    
        private RedisTemplate redisTemplate;
    
        public LoginInterceptor(RedisTemplate redisTemplate){
            this.redisTemplate=redisTemplate;
        }
    
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //设置编码
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/json;charset=utf-8");
    
            //1、判断是否携带token
            String token = request.getHeader("authorization");
            System.out.println(token);
            if(token==null || "".equals(token)){
                RestData restData = RestData.builder().code("401").msg("你未登录").build();
                String jsonRestData = JSONObject.toJSONString(restData);
                response.setStatus(401);
                response.getWriter().write(jsonRestData);
                return false;
            }
            Map<String, Object> userMap=redisTemplate.opsForHash().entries(token);
            RestSysUser restSysUser = BeanUtil.fillBeanWithMap(userMap, new RestSysUser(), false);
            //2、判断redis里面是否存在token
            if(userMap.isEmpty()){
                RestData restData = RestData.builder().code("401").msg("你未登录").build();
                String jsonRestData = JSONObject.toJSONString(restData);
                response.setStatus(401);
                response.getWriter().write(jsonRestData);
                return false;
            }
    
            //3、判断账号情况
            if(restSysUser.getUsername()!=null){
                //获取数据库账号情况
                //比对,如果账号异常,则不能访问
                //return false;
            }
            return true;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    4、用自定义注解以及切面判断该用户有没有该方法的访问权限

    下面,来介绍如何通过aop切面的方式来解决是否含有这个权限

    其中aop依赖

    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.1、自定义注解

    4.1、配置自定义注解接口

    package com.melody.rest.annotion;
    
    import java.lang.annotation.*;
    
    @Target({ ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface AuthCheck {
    
        public String value() default "";
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. @Target,说明了Annotation所修饰的对象范围
    2. @Retention,定义了该Annotation生命周期(编译/运行)
    3. @Documented,是一个标记注解,没有成员
    4. @Inherited,阐述了某个被标注的类型是被继承的。

    4.2、Aop切面:方法配置

    @Aspect 表示这是一个切面类
    @Around("@annotation(com.hmdp.annotation.MyPermission)")里面表示在注解处环绕通知。

    @Slf4j 是Lombok的关于slfj的简略写法。

    方法配置

    package com.melody.rest.aspect;
    
    
    import cn.hutool.core.bean.BeanUtil;
    import com.melody.rest.annotion.AuthCheck;
    import com.melody.rest.domain.RestSysUser;
    import com.melody.rest.exception.AuthException;
    import com.melody.rest.model.ResCode;
    import com.melody.rest.model.ResJson;
    import com.melody.rest.model.ResultCode;
    import com.melody.rest.model.ResultJson;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.util.Map;
    
    @Aspect
    @Component
    public class AuthAspect {
    
        @Value("${token.header}")
        //@Value("authorization")
        private String header;
    
        @Autowired
        private HttpServletRequest request;
    
        @Resource
        private RedisTemplate redisTemplate;
    
        
         /**
         * 目标方法
         */
        @Pointcut("@annotation(com.melody.rest.annotion.AuthCheck)")
        public void authPointCut(){
    
        }
    
       
    
        /**
         * 目标方法调用之前执行
         */
        @Before("authPointCut()")
        public void doBefore() {
            System.out.println("================== step 2: before ==================");
        }
    
        /**
         * 目标方法调用之后执行
         */
        @After("authPointCut()")
        public void doAfter() {
            System.out.println("================== step 4: after ==================");
        }
    
        /**
         * 环绕
         * 会将目标方法封装起来
         * 具体验证业务数据
         */
        @Around("authPointCut()")
        public Object authCheck(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            try{
                // 判断 TOKEN
                String token = request.getHeader(header);
                Map<String, Object> userMap=redisTemplate.opsForHash().entries(token);
                RestSysUser restSysUser = BeanUtil.fillBeanWithMap(userMap, new RestSysUser(), false);
                if(restSysUser.getUsername() == null || restSysUser.getUsername().equals("")){
                    throw new AuthException(ResCode.TOKEN_NOT_EXIST);
                } else {
                    if(restSysUser.getResources()==null){
                        throw new AuthException(ResCode.BANED_REQUEST);
                    }
                    //从切面织入点处通过反射机制获取织入点处的方法
                    MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
                    //获取切入点所在的方法
                    Method method = signature.getMethod();
                    AuthCheck ac = method.getAnnotation(AuthCheck.class);
                    boolean flag = false;
                    if(ac != null) {
                        String auth = ac.value();
                        flag = restSysUser.getResources().stream().anyMatch(str -> str.equals(auth));
                        // way2:数据库中存放权限字段,根据注解的value确定请求所需权限判断是否有权限进行访问
                    }
                    if(!flag) {
                        //throw new AuthException(ResCode.BANED_REQUEST);
                        return ResultJson.failure(ResultCode.FORBIDDEN);
                    }
                }
            } catch(AuthException e) {
                System.err.println(e.getResCode().getCode() + ":" + e.getResCode().getMsg());
                return ResJson.no(e.getResCode());
            }
            Object res = proceedingJoinPoint.proceed();
            return res;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    4.3、测试controller类的编写

    package com.melody.rest.restcontroller;
    
    import com.melody.rest.annotion.AuthCheck;
    import com.melody.rest.model.ResultJson;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    
    @RestController
    @Api(value = "测试",tags = {"测试"})
    @RequestMapping("/testRest")
    public class TestRestController {
    
    
    
        //测试方法1
        @AuthCheck("/testRest/t1")
        @ApiOperation(value = "t1测试方法")
        @PostMapping(value = "/t1")
        public ResultJson test(){
            return ResultJson.ok("test1访问成功");
        }
    
        //测试方法2
        @AuthCheck("/testRest/t10")
        @ApiOperation(value = "t10测试方法")
        @PostMapping(value = "/t10")
        public ResultJson test2(){
            return ResultJson.ok("test10访问成功");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    5、测试开始

    从图中可以看出,我们模拟用户所拥有的权限是testRest/test1

    在这里插入图片描述

    用postman发起请求:

    (1)一开始访问没有token,显示未登录

    在这里插入图片描述

    (2)登录获取token
    在这里插入图片描述

    (3)t1方法访问成功

    在这里插入图片描述

    (4)t10方法没有权限,不能访问

    在这里插入图片描述

  • 相关阅读:
    ubuntu系统下opencv的编译安装
    串口协议、I2C协议、SPI协议总结
    iptables设置黑白名单
    Linux操作系统的基础知识
    Ranger功能验证
    关于OxyPlot.Wpf包没有Plot控件问题
    C语言选择排序
    【C++】list
    Flutter教程之使用不同的方法维护 Flutter 应用程序状态
    nginx实现灰度上线(InsCode AI 创作助手)
  • 原文地址:https://blog.csdn.net/qq_45830276/article/details/126556230