基于角色的权限控制 (RBAC)

        前些天做课设了解到了一些关于权限控制的知识,开始想着看看若依框架是怎么实现的,奈何级别不够看完还是不很了解,于是上网搜搜相关文章,然后通过借助AI理解了其前后端的大致流程,最后还是基于自己的理解实现了一个差不多RBAC框架,在以后的项目中可以引用一下...

        基于角色的权限控制(Role-Based Access Control, RBAC)是一种广泛使用的访问控制机制,它通过定义角色并将权限分配给角色,再将角色分配给用户来管理系统权限

RBAC 核心概念

  1. 用户(User): 系统的使用者

  2. 角色(Role): 权限的集合,如"管理员"、"编辑"、"访客"等

  3. 权限(Permission): 对特定资源的具体操作权限,如"创建文章"、"删除用户"

  4. 会话(Session): 用户激活角色的过程

RBAC 前后端交互流程

        基于角色的权限控制,用户拥有不同的角色,不同角色有不同的权限,用户根据角色获得权限:

         首先,后端在用户登录时,根据用户的角色查询相应的权限。其次,将权限集合返回给前端。接着,前端拿到权限集合将其出入到一个useAuthStore的持久化存储中,并且里面有一个方法hasPermission(requiredPerms)判断是否拥有此权限。然后,定义路由的规则,路由守卫检查权限,如果有权限则可以跳转到路由,这样就实现了页面级的路由权限;那么如果想要实现组件级的页面权限首先,还要定义一个组件函数permission,里面根据useAuthStore.hasPermission(requiredPerms)是否拥有此权限来判断是否因隐藏此组件;不止是前端进行权限校验,后端也需要进行权限校验:首先定义一个注解@RequiresPermission ,与PermissionAspect 切面类进行绑定,切面类中进行权限校验:获取用户的权限列表,若没有该接口要求的权限,则禁止访问。

  1. 后端权限控制

    • ✔️ 登录时查询角色权限 → 返回权限集合

    • ✔️ 使用@RequiresPermission注解 + AOP切面校验

    • ✔️ 后端做最终权限防线(最关键)

  2. 前端权限控制

    • ✔️ 登录后存储权限到useAuthStore(Pinia持久化)

    • ✔️ 路由守卫做页面级拦截

    • ✔️ permission组件做元素级控制

基于Springboot+Vue3的RBAC实现

1.数据库表设计

        这里我没有使用物理外键,其中用户和角色多对多关系,角色和权限多对多关系。

现在我模拟角色vip1有look1权限,vip2有look1和look2权限,vip3有look1、look2和look3权限;一个用户hl拥有vip1和vip2角色:

2.后端登录返回权限集合逻辑

        后端在登录成功后,生成jwt令牌,并随着用户所拥有的角色集合和对应的权限集合一起存入Redis中,并返回给前端

public Result<UserVO> login(@RequestBody User user) {
        log.info("用户登录:{}", user);
        User u=userService.login(user);
        //判断用户是否存在
        if(Objects.isNull(u)) {
            throw new RuntimeException("用户不存在!");
        }
        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", u.getUserId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims);
        // 查询权限集合
        List<String> permissions = userService.permissionQuery(u.getUserId());
        // 查询角色集合
        List<String> roles = userService.roleQuery(u.getUserId());
        UserVO userVO = UserVO.builder()
                .userName(user.getUserName())
                .permissions(permissions)
                .roles(roles)
                .token(token)
                .build();
        //将token存入redis
        String key = "login:"+u.getUserId();
        redisCache.setCacheObject(key, userVO);
        return Result.success(userVO);
    }

3.前端持久化存储

        我使用pinia持久化存储,将登录后返回的用户信息存储,并增加了一个方法hasPermission(requiredPerms)判断是否拥有此权限:

import {defineStore} from 'pinia'
import {ref} from 'vue'
const useUserInfoStore = defineStore('userInfo',()=>{
    //定义状态相关的内容

    const info = ref({})

    const setInfo = (newInfo)=>{
        info.value = newInfo
    }


    const removeInfo = ()=>{
        info.value = {}
    }
    // 判断用户权限
    const hasPermission = (permission) => {
        return info.value.permissions?.includes(permission)
    }

    return {info,setInfo,removeInfo,hasPermission}

},{persist:true})

export default useUserInfoStore;

4.页面级路由权限

        定义路由守卫检查权限,如果有权限则可以跳转到路由,这里我创建了6个页面,其中V1是登录后进入的页面,teacher、student和manager是点击V1中的按钮进行跳转的页面:

以下是V1页面:

以下是关于路由的配置,这里我设置teacher、student和manager都是需要权限才能进行页面跳转:

//导入vue-router
import {createRouter, createWebHistory} from 'vue-router'
//导入组件
import StudentVue from '@/views/student.vue'
import Teacher from "@/views/teacher.vue";
import Manage from "@/views/Manage.vue";
import V1 from "@/views/V1.vue";
import Error from "@/views/Error.vue";
import useUserInfoStore from '@/stores/userInfo.js';
import Login from "@/views/Login.vue";


//定义路由关系
const routes = [
    {path: '/login',name: 'login', component: Login},
    {path: '/',name: 'v1', component: V1},
    {path: '/teacher',name: 'teacher', component: Teacher, meta: {requiresAuth: true,permissions: ['look1']}},
    {path: '/student',name: 'student', component: StudentVue, meta: {requiresAuth: true,permissions: ['look2']}},
    {path: '/manage',name: 'manage', component: Manage, meta: {requiresAuth: true,permissions: ['look3']}},
    {path: '/error', name: 'error', component: Error}
]

//创建路由器
const router = createRouter({
    history: createWebHistory(),
    routes: routes
});


router.beforeEach((to, from, next) => {
    const  userInfoStore = useUserInfoStore();
    // 如果目标路由不是登录页且没有token,则跳转到登录页
    if (to.name !== 'login' && !userInfoStore.info.token ) {
      next({ name: 'login' });
      return;
    }
    // 如果路由不需要权限,直接放行
    if (!to.meta.requiresAuth) {
        next();
        return;
    }
    // 需要权限的路由:检查用户是否有对应权限
    const requiredPermissions = to.meta.permissions;

    // 遍历检查用户是否拥有所有必需的权限
    const hasPermission = requiredPermissions.every(permission =>
        userInfoStore.hasPermission(permission)
    );

    if (hasPermission) {
        next(); // 有权限,放行
    } else {
        next({ name: 'error' }); // 无权限,跳转到错误页或其他提示
    }

  })

export default router

通过V1页面我们可以看到当前用户只有look1和look2权限,根据路由的配置我们是不能访问manager页面的,当我们点击前往去校长界面按钮时,就会跳转到错误页面:

5.组件级页面权限

        组件级页面权限就是我们根据用户的权限来隐藏一些组件。

        要实现组件级的页面权限,首先,还要定义一个组件函数permission,里面根据useAuthStore.hasPermission(requiredPerms)是否拥有此权限来判断是否因隐藏此组件。

下面是注册指令permission:

import useUserInfoStore from '@/stores/userInfo.js'

export default {
  mounted(el, binding) {
    const requiredPermission = binding.value; // 获取传入的权限字符串

    const userInfoStore = useUserInfoStore();

    // 如果没有权限,则移除该元素
    if (!userInfoStore.hasPermission(requiredPermission)) {
      el.parentNode?.removeChild(el)
    }
  }
}
import permission from './directives/permission.js'

// 注册权限指令
app.directive('permission', permission)

使用指令给我们的组件加上权限访问,如下我给点击去校长界面的按钮加上了需要look3的权限,可以看到刚才的按钮被移除了:

<div>
      <el-button type="primary" @click="handleClick1">点击去老师界面</el-button>
    </div>
    <div>
      <el-button type="primary" @click="handleClick2">点击去学生界面</el-button>
    </div>
    <div>
      <el-button type="primary" v-permission="'look3'" @click="handleClick3">点击去校长界面</el-button>
    </div>

6.后端权限校验

        前后端一起进行权限校验才会更加安全,这部分逻辑将使用AOP来完成。

        首先定义一个注解@RequiresPermission ,与PermissionAspect 切面类进行绑定,切面类中进行权限校验:获取用户的权限列表,若没有该接口要求的权限,则禁止访问。

6.1 基于角色校验

以下是@RequiresRoles注解,定义在方法上,用于判断是否有权限访问该接口:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresRoles {
    String[] value() default {};
}
**
 * 用于角色权限校验
 * */

@Slf4j
@Aspect
@Component
public class RolesAuthAspect {
    @Autowired
    private RedisCache redisCache;

    @Before("@annotation(requiresRoles)")
    public void checkRolesPermission(JoinPoint jp, RequiresRoles requiresRoles) {
        // 获取当前用户
        Long userId = BaseContext.getCurrentId();
        UserVO userVO = redisCache.getCacheObject("login:" + userId);
        // 获取用户角色
        List<String> roles = userVO.getRoles();
        // 检查权限
        String[] reRoles = requiresRoles.value();
        boolean hasPermission  = Arrays.stream(reRoles).allMatch(roles::contains);
        if (!hasPermission){
            throw new BaseException("权限不足!");
        }

    }

}

下面我们使用apifiox进行验证,我定义了三个接口,其中vip3是我们当前用户不能访问的:

@RequiresRoles({"vip1"})
    @GetMapping("/vip1")
    public Result vip1() {
        log.info("vip1");
        return Result.success("vip1成功");
    }
    @RequiresRoles({"vip2"})
    @GetMapping("/vip2")
    public Result vip2() {
        log.info("vip2");
        return Result.success("vip2成功");
    }
    @RequiresRoles({"vip3"})
    @GetMapping("/vip3")
    public Result vip3() {
        log.info("vip3");
        return Result.success("vip3成功");
    }

6.2 基于权限校验

以下是@RequiresPermissions注解,定义在方法上,用于判断是否有权限访问该接口:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresPermissions {
    String[] value() default {};
}
/**
 * 用于角色分配的权限校验
 * */
@Slf4j
@Aspect
@Component
public class PermissionsAuthAspect {

    @Autowired
    private RedisCache redisCache;

    @Before("@annotation(requiresPermissions)")
    public void checkRolesPermission(JoinPoint jp, RequiresPermissions requiresPermissions) {
        // 获取当前用户
        Long userId = BaseContext.getCurrentId();
        UserVO userVO = redisCache.getCacheObject("login:" + userId);
        // 获取用户角色权限
        List<String> permissions = userVO.getPermissions();
        // 检查权限
        String[] rePermissions = requiresPermissions.value();
        boolean hasPermission  = Arrays.stream(rePermissions).allMatch(permissions::contains);
        if (!hasPermission){
            throw new BaseException("权限不足!");
        }

    }
}

下面我们使用apifiox进行验证,我定义了三个接口,其中look3是我们当前用户不能访问的:

    @RequiresPermissions({"look1"})
    @GetMapping("/look1")
    public Result look1() {
        log.info("look1");
        return Result.success("look1成功");
    }
    @RequiresPermissions({"look2"})
    @GetMapping("/look2")
    public Result look2() {
        log.info("look2");
        return Result.success("look2成功");
    }
    @RequiresPermissions({"look3"})
    @GetMapping("/look3")
    public Result look3() {
        log.info("look3");
        return Result.success("look3成功");
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值