RBAC 模型基本介紹
RBAC(Role-Based Access Control,基于角色的訪問控制)是一種廣泛應用的權限管理模型。它的核心思想是通過角色來管理權限,而不是直接分配權限給用戶。用戶被賦予一個或多個角色,而每個角色擁有不同的權限。
RBAC 的核心組件
用戶(User):系統的使用者。
角色(Role):一組權限的集合。
權限(Permission):對系統資源的具體操作(如讀、寫、刪除等)。
用戶-角色分配(User-Role Assignment):將用戶與角色關聯。
角色-權限分配(Role-Permission Assignment):將角色與權限關聯。
RBAC 的工作流程
- 管理員定義角色(如管理員、編輯、訪客)。
- 為每個角色分配相應的權限(如管理員可以讀寫,訪客只能讀)。
- 將用戶分配到相應的角色。
- 用戶通過角色間接獲得權限
舉例:
場景:
存在某系統,系統包含 1 ~ 10 個菜單目錄,現在用戶群體 A 需要擁有 1~3 的菜單權限,用戶群體 B 需要擁有 3~5 菜單的權限,用戶群體 C 需要擁有 6~10 的菜單權限…
RBAC 處理方式:
- 系統管理員定義角色,為用戶群體 A 創建角色 A1,A1包含 1~3 的菜單權限,為用戶群里 B 創建角色 A2…
- 將角色分配到不同用戶群體。
- 不同用戶根據分配到的角色,間接獲得系統的部分權限。
RBAC 優缺點
優點
- 簡化權限管理:通過角色間接分配權限,減少了直接管理用戶權限的復雜性。
- 易于擴展:新增用戶只需分配角色,無需重新定義權限。
- 支持最小權限原則:可以為角色分配最小必要的權限,降低安全風險。
- 適合多用戶系統:在企業級應用中,RBAC 可以很好地支持復雜的權限需求。
缺點
- 角色爆炸問題:當角色過多時,管理角色本身會變得復雜。
- 靈活性不足:對于需要動態權限的場景(如基于時間、地點的權限),RBAC 的支持較弱。
- 權限粒度有限: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){// 先獲取當前是哪個賬號idObject 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