# 菜单权限控制说明 ## 📋 概述 系统通过 **权限编码(Permission Code)** 来控制用户对菜单的访问。菜单权限控制分为两个层面: 1. **菜单显示控制**:根据用户权限过滤菜单,只显示用户有权限访问的菜单 2. **路由访问控制**:通过路由守卫检查用户是否有权限访问某个页面 ## 🔄 权限控制流程 ``` 用户登录 ↓ 获取用户角色和权限 ↓ 调用 /api/menus/user-menus 获取用户菜单 ↓ 后端根据用户权限过滤菜单 ↓ 前端动态生成路由和菜单 ↓ 路由守卫检查页面访问权限 ``` ## 🎯 如何配置菜单权限 ### 1. 创建权限 首先需要在权限管理中创建权限,例如: - `menu:read` - 查看菜单权限 - `user:read` - 查看用户权限 - `role:read` - 查看角色权限 ### 2. 将权限分配给角色 在角色管理中,为角色分配相应的权限。例如: - **管理员角色**:拥有所有权限 - **普通用户角色**:只拥有 `user:read` 权限 ### 3. 为用户分配角色 在用户管理中,为用户分配角色。用户会继承角色的所有权限。 ### 4. 为菜单设置权限编码 在菜单管理中,为菜单设置 `权限编码` 字段: #### 示例配置 | 菜单名称 | 路径 | 权限编码 | 说明 | | -------- | ------------------- | ----------- | --------------------------------------------- | | 用户管理 | /system/users | `user:read` | 只有拥有 `user:read` 权限的用户才能看到此菜单 | | 角色管理 | /system/roles | `role:read` | 只有拥有 `role:read` 权限的用户才能看到此菜单 | | 权限管理 | /system/permissions | - | 不设置权限编码,所有用户都可以看到 | | 仪表盘 | /dashboard | - | 不设置权限编码,所有用户都可以看到 | ### 5. 权限编码规则 权限编码格式:`资源:操作` 常见示例: - `user:read` - 查看用户 - `user:create` - 创建用户 - `user:update` - 更新用户 - `user:delete` - 删除用户 - `role:read` - 查看角色 - `menu:read` - 查看菜单 ## 💻 技术实现 ### 后端实现 #### 1. 菜单权限过滤(`MenusService.findUserMenus`) ```typescript // 获取用户的所有权限 const userPermissions = await this.authService.getUserPermissions(userId); // 过滤菜单:如果菜单有permission字段,检查用户是否有该权限 const filterMenus = (menus: any[]): any[] => { return menus .filter((menu) => { // 如果菜单没有设置权限要求,则显示 if (!menu.permission) { return true; } // 如果设置了权限要求,检查用户是否有该权限 return userPermissions.includes(menu.permission); }) .map((menu) => { // 递归过滤子菜单 if (menu.children && menu.children.length > 0) { menu.children = filterMenus(menu.children); } return menu; }); }; ``` #### 2. 用户权限获取(`AuthService.getUserPermissions`) ```typescript async getUserPermissions(userId: number): Promise { const user = await this.usersService.findOne(userId); const permissions = new Set(); // 遍历用户的所有角色 user.roles?.forEach((ur: any) => { // 遍历角色的所有权限 ur.role.permissions?.forEach((rp: any) => { permissions.add(rp.permission.code); }); }); return Array.from(permissions); } ``` ### 前端实现 #### 1. 菜单转换为路由(`convertMenusToRoutes`) ```typescript const route: RouteRecordRaw = { path: routePath, name: routeName, meta: { title: menu.name, requiresAuth: true, // 如果菜单有权限要求,添加到路由meta中 ...(menu.permission && { permissions: [menu.permission] }), }, component: componentLoader, }; ``` #### 2. 路由守卫检查(`router.beforeEach`) ```typescript // 检查权限 const requiredPermissions = to.meta.permissions; if (requiredPermissions && requiredPermissions.length > 0) { if (!authStore.hasAnyPermission(requiredPermissions)) { // 没有所需权限,跳转到 403 页面 next({ name: "Forbidden" }); return; } } ``` #### 3. 权限检查方法(`authStore`) ```typescript // 检查是否有指定权限 const hasPermission = (permission: string): boolean => { return user.value?.permissions?.includes(permission) ?? false; }; // 检查是否有任一权限 const hasAnyPermission = (permissions: string[]): boolean => { if (!permissions || permissions.length === 0) return true; return permissions.some((perm) => hasPermission(perm)); }; ``` ## 📝 使用示例 ### 示例 1:创建需要权限的菜单 1. 登录系统,进入 **菜单管理** 2. 点击 **新增菜单** 3. 填写菜单信息: - 菜单名称:`用户管理` - 路由路径:`/system/users` - 组件路径:`system/users/Index` - **权限编码**:`user:read` ⭐ - 父菜单:选择 `系统管理` 4. 保存菜单 ### 示例 2:创建公开菜单(所有用户可见) 1. 在菜单管理中新增菜单 2. **权限编码字段留空** 3. 这样所有用户都可以看到此菜单 ### 示例 3:为用户分配权限 1. 进入 **权限管理**,创建权限: - 权限名称:`查看用户` - 权限编码:`user:read` - 资源:`user` - 操作:`read` 2. 进入 **角色管理**,编辑角色: - 为角色分配 `user:read` 权限 3. 进入 **用户管理**,编辑用户: - 为用户分配该角色 ## ⚠️ 注意事项 1. **权限编码必须唯一**:每个权限编码在系统中是唯一的 2. **菜单权限为空则公开**:如果菜单的 `权限编码` 字段为空,所有用户都可以看到 3. **子菜单继承父菜单权限**:子菜单会独立检查权限,不会自动继承父菜单权限 4. **路由和菜单双重控制**: - 菜单显示控制:控制菜单是否在侧边栏显示 - 路由访问控制:控制用户是否可以直接访问页面(通过 URL) 5. **权限变更后需重新登录**:权限变更后,用户需要重新登录才能看到新的菜单 ## 🔍 调试技巧 ### 1. 查看用户权限 在浏览器控制台执行: ```javascript // 查看当前用户权限 console.log(useAuthStore().user?.permissions); // 检查是否有特定权限 console.log(useAuthStore().hasPermission("user:read")); ``` ### 2. 查看用户菜单 ```javascript // 查看当前用户的菜单 console.log(useAuthStore().menus); ``` ### 3. 后端调试 在后端日志中查看: - 用户权限列表 - 菜单过滤结果 ## 📚 相关文档 - [RBAC 权限控制详解](./RBAC_GUIDE.md) - [菜单管理使用说明](./MENU_MANAGEMENT.md) - [权限管理使用说明](./PERMISSION_MANAGEMENT.md)