SpringBoot基于RBAC自制简易的权限验证(AOP自定义注解)

发布于:2023-01-04 ⋅ 阅读:(427) ⋅ 点赞:(0)

SpringBoot基于RBAC自制简易的权限验证(AOP自定义注解)

之前的博客介绍了RBAC的原理,这个博客介绍一下springboot如何基于RBAC自制简易的权限验证

流程介绍

image-20220826135914592

这里做了一些简单的修改,比如redis存入的key是token,value是对应的user对象

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

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

坐标依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3</version>
    </dependency>
    <!--hutool-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.7.17</version>
    </dependency>
    <!-- aop相关       -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.1</version>
    </dependency>
</dependencies>

登录认证

登录函数

public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.校验验证码
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
//        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误");
        }
        // select * from tb_user where phone = ?
        //一致,根据手机号查询用户(query获取表名通过User实体的@tableName注解,ServiceImpl<UserMapper, User>)
        //one返回一个对象,list,返回集合
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if(user == null){
            //不存在,则创建
            user =  createUserWithPhone(phone);
        }
        //7.保存用户信息到session中
        // BeanUtil.copyProperties(user, UserDTO.class)(hutol工具类,把user的内容拷贝到UserDto)
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        //7.保存用户信息到redis中
        // 7.1.随机生成token,作为登录令牌(使用hutol提供的UUid,toString(true)标识生成的没有-符号,toString()有)
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        //赋值user给userDto(去掉敏感信息)
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //转为HashMap存储(因为stringRedisTemplate要求key和value存储的都是string类型,需要转换)
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)//忽略一些空值
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//setFieldValueEditor传入字段名和字段值,只要字符串形式的字段值
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;

        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期(hash不能直接设置)
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

配置拦截器

这里我用到了两个拦截器,因为我的token存在redis里,不能定时刷新,所以一个拦截器用来刷新redis(拦截全部请求),一个拦截用来拦截全部请求,验证用户

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }

}

RefreshTokenInterceptor

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    //这个类没有加上@compentent类似的注解,该类对象的属性注入不有spring来完成,只能通过构造方法的方式,来初始化成员变量
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    //.if生成if表达式   .var生成变量
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        //这里不拦截,第二个拦截器进行拦截
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        //entries取出该hash的key的全部字段值,返回值是map集合,get只能取出key的单个字段值,返回值是Object
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在,opsForHash如果为null会返回一个空的map,所以这里不判断Null
        //这个拦截器不拦截
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

MvcConfig(拦截器配置)

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(//排除下面不需要拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        //order()配置这个拦截器执行顺序,值越大,优先级越低
        // token刷新的拦截器
        //,addPathPatterns//添加拦截/**拦截全部,默认不写也是拦截全部请求
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

以上为登录判断,存入token

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

资源认证

其中aop依赖

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

自定义注解

自定义了几个常量

public class PermissionConsts {
 
    /**
     * 查看权限
     */
    public static final String R = "R_PERMISSION";
 
    /**
     * 添加权限
     */
    public static final String C = "C_PERMISSION";
 
    /**
     * 修改权限
     */
    public static final String U = "U_PERMISSION";
 
    /**
     * 删除权限
     */
    public static final String D = "D_PERMISSION";
}

MyPermission

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPermission {

    /**
     * 默认只有查看权限
     * @return
     */
    String value() default PermissionConsts.R;
}
  1. @Target,说明了Annotation所修饰的对象范围
  2. @Retention,定义了该Annotation生命周期(编译/运行)
  3. @Documented,是一个标记注解,没有成员
  4. @Inherited,阐述了某个被标注的类型是被继承的。

AOP切面

@Aspect 表示这是一个切面类
@Around("@annotation(com.hmdp.annotation.MyPermission)")里面表示在注解处环绕通知。
@Slf4j 是Lombok的关于slfj的简略写法。

@Aspect
@Component
@Slf4j
public class PermissionAspect {

    @Resource
    private IUserService userService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ResourceVerification resourceVerification;

    /**
     * 目标方法
     */
    @Pointcut("@annotation(com.hmdp.annotation.MyPermission)")
    private void permission() {

    }

    /**
     * 目标方法调用之前执行
     */
    @Before("permission()")
    public void doBefore() {
        System.out.println("================== step 2: before ==================");
    }

    /**
     * 目标方法调用之后执行
     */
    @After("permission()")
    public void doAfter() {
        System.out.println("================== step 4: after ==================");
    }

    /**
     * 环绕
     * 会将目标方法封装起来
     * 具体验证业务数据
     */
    @Around("permission()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("================== step 1: around ==================");
        long startTime = System.currentTimeMillis();
        /*
         * 解析token :
         * 1、token是否存在
         * 2、token格式是否正确
         * 3、token是否已过期(解析信息或者redis中是否存在)
         * */

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");

        if (StrUtil.isEmpty(token)) {
            throw new RuntimeException("非法请求,无效token");
        }
        // 校验token的业务逻辑\
        // 基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        //entries取出该hash的key的全部字段值,返回值是map集合,get只能取出key的单个字段值,返回值是Object
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()) {
//            throw new RuntimeException("非法请求,无效用户");
            System.out.println("非法请求,无效用户");
        }
        // 解析token之后,获取当前用户的账号信息,查看它对应的角色和权限信息
        // 将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        //查询,用户的权限信息
        Long userId = userDTO.getId();
        // List<String> permissionCodes = userService.getResource(userId);

        // 这块没有对应的数据表,用写死的集合表示。

        /*
         * 获取注解的值,并进行权限验证
         * */
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        MyPermission myPermission = method.getAnnotation(MyPermission.class);
        String value = myPermission.value();
        // 将注解的值和token解析后的值进行对比,查看是否有该权限,如果权限通过,允许访问方法;否则不允许,并抛出异常
        //这里写死比较,实际查询数据库
        if(!resourceVerification.compareResource(value)){
//            throw new RuntimeException("对不起,您没有权限访问!");
            System.out.println("对不起,您没有权限访问!");
        }
        // 执行具体方法
        Object result = proceedingJoinPoint.proceed();

        long endTime = System.currentTimeMillis();

        /*
         * 记录相关执行结果
         * 可以存入MongoDB 后期做数据分析
         * */
//        // 打印请求 url
//        System.out.println("URL            : " + request.getRequestURL().toString());
//        // 打印 Http method
//        System.out.println("HTTP Method    : " + request.getMethod());
//        // 打印调用 controller 的全路径以及执行方法
//        System.out.println("controller     : " + proceedingJoinPoint.getSignature().getDeclaringTypeName());
//        // 调用方法
//        System.out.println("Method         : " + proceedingJoinPoint.getSignature().getName());
//        // 执行耗时
//        System.out.println("cost-time      : " + (endTime - startTime) + " ms");
        return result;
    }
}

这里没有去查询数据库查询用户的资源,二是写了一个死的集合来判断

ResourceVerification

@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("/user/me");
        resources.add("/user/test");
        resources.add("/test/me");
        resources.add("/test/test");
        resources.add("/test/me");
    }
}

测试函数

 @GetMapping("/me")
    @MyPermission("/user/me122")
    // @MyPermission自定义切面注解
    public Result me(){
        // TODO 获取当前登录的用户并返回(从ThreadLocal中)
        UserDTO user = UserHolder.getUser();
//        return Result.fail("功能未完成");
        return Result.ok(user);
    }

image-20220826160027034

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到