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.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.12</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.9</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjtools -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.8.9</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

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);
    }

}

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);
        }
    }

其中 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;
    }
}

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);


    }
}

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;
    }
}

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

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

其中aop依赖

<!-- aop相关       -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

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. @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;
    }

}

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访问成功");
    }

}

5、测试开始

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

在这里插入图片描述

用postman发起请求:

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

在这里插入图片描述

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

(3)t1方法访问成功

在这里插入图片描述

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

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐