当前位置: 首页 > news >正文

RBAC 模型的简单实现

RBAC 模型基本介绍

RBAC(Role-Based Access Control,基于角色的访问控制)是一种广泛应用的权限管理模型。它的核心思想是通过角色来管理权限,而不是直接分配权限给用户。用户被赋予一个或多个角色,而每个角色拥有不同的权限。

RBAC 的核心组件

用户(User):系统的使用者。
角色(Role):一组权限的集合。
权限(Permission):对系统资源的具体操作(如读、写、删除等)。
用户-角色分配(User-Role Assignment):将用户与角色关联。
角色-权限分配(Role-Permission Assignment):将角色与权限关联。

RBAC 的工作流程

  1. 管理员定义角色(如管理员、编辑、访客)。
  2. 为每个角色分配相应的权限(如管理员可以读写,访客只能读)。
  3. 将用户分配到相应的角色。
  4. 用户通过角色间接获得权限

举例:

场景:
存在某系统,系统包含 1 ~ 10 个菜单目录,现在用户群体 A 需要拥有 1~3 的菜单权限,用户群体 B 需要拥有 3~5 菜单的权限,用户群体 C 需要拥有 6~10 的菜单权限…


RBAC 处理方式:

  1. 系统管理员定义角色,为用户群体 A 创建角色 A1,A1包含 1~3 的菜单权限,为用户群里 B 创建角色 A2…
  2. 将角色分配到不同用户群体。
  3. 不同用户根据分配到的角色,间接获得系统的部分权限。

RBAC 优缺点

优点

  1. 简化权限管理:通过角色间接分配权限,减少了直接管理用户权限的复杂性。
  2. 易于扩展:新增用户只需分配角色,无需重新定义权限。
  3. 支持最小权限原则:可以为角色分配最小必要的权限,降低安全风险。
  4. 适合多用户系统:在企业级应用中,RBAC 可以很好地支持复杂的权限需求。

缺点

  1. 角色爆炸问题:当角色过多时,管理角色本身会变得复杂。
  2. 灵活性不足:对于需要动态权限的场景(如基于时间、地点的权限),RBAC 的支持较弱。
  3. 权限粒度有限:RBAC 的权限控制通常是粗粒度的,难以实现细粒度的权限控制。

RBAC 的实现

数据库设计

图中由左至右,依次为:菜单表、菜单-角色对应表、角色表、角色-用户对应表、用户表;
菜单表和角色表之间为多对多关系;
角色表和用户表之间为多对多关系;
在这里插入图片描述

后端实现(SpringBoot + SaToken)

    @PostMapping("/addMenu")  
    @Operation(summary = "新增菜单")
    @SaCheckPermission("system:menu:add")   // 只有用户含有该权限标识符,才会放行请求,否则抛出异常
    public ResponseResult addMenu(@Validated @RequestBody MenuForm menuForm) {
        log.info("addMenu:{}", menuForm);
        menuService.addMenu(menuForm, StpUtil.getLoginId().toString());
        return ResponseResult.ok().message("添加成功");
    }

SaCheckPermission实现原理

/** SaToken 处理逻辑 **/
	public void checkPermissionAnd(String... permissionArray){
		// 先获取当前是哪个账号id
		Object loginId = getLoginId();

		// 如果没有指定权限,那么直接跳过
		if(permissionArray == null || permissionArray.length == 0) {
			return;
		}

		// 获取该用户的所有权限
 		List<String> permissionList = getPermissionList(loginId);
 		for (String permission : permissionArray) {
 			// 判断该权限是否在该用户的权限列表中,若不存在,则抛出 NotPermissionException 异常
 			if(!hasElement(permissionList, permission)) {
 				throw new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051);
 			}
 		}
 	}

前端实现

/** 前端动态路由 **/
const dynamicRoutesFromBackend = (backendRoutes: MenuEntity[]): RouteType[] => {
    // 辅助函数:如果路径以 '/' 开头,则去掉 '/'
    const normalizePath = (path?: string): string => {
        return path && path.startsWith('/') ? path.slice(1) : path || ''; // 确保路径存在后再处理
    };
    // 生成动态路由
    const dynamicChildren: RouteType[] = [];
    backendRoutes
        .filter(route => route.status === 1)
        .forEach(route => {
        if (route.menuType === 'M') {
            const normalizedPath = normalizePath(route.path);
            if (normalizedPath && !route.children) {
                dynamicChildren.push({
                    path: normalizedPath,
                    element: withLoadingComponent(
                        lazy(() =>
                            (modules[`/src/views/${normalizedPath}/index.tsx`]?.()
                                .then((module) => ({ default: (module as { default: React.ComponentType }).default })))  // 类型断言
                            || import('../views/Error')
                                .then((module) => ({ default: (module as { default: React.ComponentType }).default }))
                        )
                    ),
                    children: []
                });
            }

            if (!route.path && route.children) {
                route.children.forEach(childRoute => {
                    const normalizedChildPath = normalizePath(childRoute.path);
                    dynamicChildren.push({
                        path: normalizedChildPath,
                        element: withLoadingComponent(
                            lazy(() =>
                                modules[`/src/views/${normalizedChildPath}/index.tsx`]
                                    ? (modules[`/src/views/${normalizedChildPath}/index.tsx`] as () => Promise<{ default: React.ComponentType }> )()  // 使用类型断言并确保返回 Promise<{ default: React.ComponentType }>
                                    : import('../views/Error').then((mod) => ({ default: mod.default }))  // 错误页面的处理
                            )
                        ),
                        children: []
                    });
                });
            }
        }
    });
/**动态加载菜单**/
function covertRoutesToMenuItems(routes: RouteType[]): MenuItem[] {
    // 查找 path 为 '/' 的根路由
    const rootRoute = routes.find(route => route.path === '/');
    if (!rootRoute || !rootRoute.children) {
        return [];
    }

    // 递归解析子路由
    const convertToMenu = (routeList: RouteType[]): MenuItem[] => {
        return routeList
            .filter(route => !route.hidden) // 过滤掉 hidden 为 true 的路由
            .map(route => ({
                key: route.path,
                label: route.name,
                icon: route.icon,
                children: route.children ? convertToMenu(route.children) : undefined,  // 递归处理子菜单
            }));
    };
    // 开始处理 '/' 的子路由
    return convertToMenu(rootRoute.children);
}

完整代码

前端完整代码地址 : Gitee
后端完整代码地址 : Gitee

相关文章:

  • Mamba组件:状态空间模型简介
  • 使用 PerformanceObserver 实现网页性能优化的最佳实践
  • java字符串案例 //要求:将输入的字符串中的数字转换为罗马数字,长度小于9(运用方法:查表法)
  • 目标检测中衡量模型速度和精度的指标:FPS和mAP
  • 2023 年全国职业院校技能大赛(中职组)移动应用与开发赛项 赛题第十套
  • 说说 CDN 的工作原理,它在前端性能优化中起到什么作用?
  • 零基础上手Python数据分析 (4):Python数据结构精讲 - 列表、元组、字典、集合
  • NVM环境下安装pnpm报错的解决方案
  • 项目开发 1-确定选题,制作原型
  • uniapp+vue实现购物车的左滑删除功能
  • BSides-Vancouver-2018 ftp匿名访问、hydra爆破22端口、nc瑞士军刀、提权
  • uniapp-x js 限制
  • 3.14-信号
  • Python Matplotlib面试题精选及参考答案
  • 删除有序数组中的重复项(26)
  • springboot树形结构 支持模糊查询,返回匹配节点和父节点,其他节点不返回
  • Python 基础语法详解
  • 掌握这些 UI 交互设计原则,提升产品易用性
  • Linux内核中断管理总
  • 射频前端模块(FEM)的基本原理与架构:从组成到WiFi路由器的应用
  • 大外交|中美联合声明拉升全球股市,专家:中美相向而行为世界提供确定性
  • 贵州省总工会正厅级副主席梁伟被查,曾任贵州省纪委副书记
  • 巴基斯坦对印度发起网络攻击,致其约70%电网瘫痪
  • 五粮液董事长:茅台1935已脱离千元价位带,五粮液在千元价位已逐步摆脱其他竞品纠缠
  • 习近平同俄罗斯总统普京会谈
  • 民生访谈|今年上海还有哪些重要演出展览?场地配套如何更给力?