# 5.4 权限管理(RBAC) :::tip 概述 系统采用 RBAC(基于角色的访问控制)权限模型,通过"用户 → 角色 → 菜单/权限"三层关系实现细粒度的权限控制。权限控制分为两个维度:**功能权限**(能做什么操作)和**数据权限**(能看到哪些数据)。 ::: ## 权限模型 ### ER 关系图 ```text ┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐ │ User │───►│ UserRole │───►│ Role │───►│ RoleMenu │───►│ Menu │ │ │ │ │ │ │ │ │ │ │ │ id │ │ user_id │ │ id │ │ role_id │ │ id │ │ username│ │ role_id │ │tenant_id│ │ menu_id │ │ perm │ │ realname│ └────────────┘ │ name │ └────────────┘ │ type │ │ dept_id │ │ code │ │ parent │ │tenant_id│ │data_scope│ │ sort │ └─────────┘ └─────────┘ └─────────┘ ``` ### 关联关系说明 | 关系 | 说明 | 中间表 | |------|------|--------| | User ↔ Role | 多对多:一个用户可有多个角色,一个角色可分配给多个用户 | `think_user_role` | | Role ↔ Menu | 多对多:一个角色可有多个菜单/权限,一个菜单可分配给多个角色 | `think_role_menu` | | User → Dept | 多对一:一个用户属于一个部门 | `dept_id` 外键 | | User → Tenant | 多对一:一个用户属于一个租户 | `tenant_id` 外键 | ### 核心表结构 **think_role(角色表):** | 字段 | 类型 | 说明 | |------|------|------| | `id` | INT | 主键 | | `tenant_id` | INT | 租户ID:0=平台级 | | `name` | VARCHAR(50) | 角色名称 | | `code` | VARCHAR(50) | 角色编码 | | `data_scope` | TINYINT | 数据权限范围:1=全部 2=本部门 3=仅本人 | | `status` | TINYINT | 状态 | | `remark` | VARCHAR(500) | 备注 | | `sort` | INT | 排序 | **think_menu(菜单/权限表):** | 字段 | 类型 | 说明 | |------|------|------| | `id` | INT | 主键 | | `parent_id` | INT | 父级 ID(0=顶级) | | `name` | VARCHAR(50) | 菜单名称 | | `permission` | VARCHAR(100) | 权限标识(如 `sys:user:add`) | | `type` | TINYINT | 类型:0=目录 1=菜单 2=按钮 | | `path` | VARCHAR(200) | 路由路径 | | `icon` | VARCHAR(100) | 图标 | | `sort` | INT | 排序 | | `status` | TINYINT | 状态 | ## 权限码格式 ```text sys:{模块名}:{操作} ``` ### 权限码命名规范 | 操作 | 权限码 | 说明 | |------|--------|------| | 分页查询 | `sys:{module}:page` | 列表页 | | 全量列表 | `sys:{module}:list` | 下拉框数据源 | | 详情 | `sys:{module}:detail` | 查看详情 | | 新增 | `sys:{module}:add` | 添加记录 | | 修改 | `sys:{module}:update` | 修改记录 | | 删除 | `sys:{module}:delete` | 删除记录 | | 批量删除 | `sys:{module}:batchDelete` | 批量删除 | | 导入 | `sys:{module}:import` | Excel 导入 | | 导出 | `sys:{module}:export` | Excel 导出 | | 重置密码 | `sys:{module}:resetPwd` | 重置密码 | ### 权限码示例 ```text 用户管理: sys:user:page 用户分页 sys:user:detail 用户详情 sys:user:add 添加用户 sys:user:update 修改用户 sys:user:delete 删除用户 sys:user:batchDelete 批量删除用户 sys:user:import 导入用户 sys:user:export 导出用户 sys:user:resetPwd 重置密码 角色管理: sys:role:page 角色分页 sys:role:add 添加角色 sys:role:update 修改角色 sys:role:delete 删除角色 文章管理: sys:article:page 文章分页 sys:article:add 添加文章 sys:article:update 修改文章 sys:article:delete 删除文章 ``` ## 菜单类型 | type | 说明 | 前端行为 | 权限校验 | |------|------|----------|----------| | 0 | 目录 | 渲染为侧边栏分组(如"系统管理") | 不参与权限校验 | | 1 | 菜单 | 渲染为可点击的菜单项(如"用户管理") | 参与权限校验(AuthMiddleware 查询 `type=1`) | | 2 | 按钮 | 不渲染,用于定义子级权限码 | 权限码挂在 type=1 的父菜单下 | ### 菜单树示例 ```text 系统管理 (type=0, 目录) ├── 用户管理 (type=1, 菜单, permission=sys:user:page) │ ├── 添加用户 (type=2, 按钮, permission=sys:user:add) │ ├── 修改用户 (type=2, 按钮, permission=sys:user:update) │ ├── 删除用户 (type=2, 按钮, permission=sys:user:delete) │ └── 重置密码 (type=2, 按钮, permission=sys:user:resetPwd) ├── 角色管理 (type=1, 菜单, permission=sys:role:page) │ ├── 添加角色 (type=2, 按钮, permission=sys:role:add) │ └── 修改角色 (type=2, 按钮, permission=sys:role:update) └── 菜单管理 (type=1, 菜单, permission=sys:menu:page) 内容管理 (type=0, 目录) ├── 文章管理 (type=1, 菜单, permission=sys:article:page) │ ├── 添加文章 (type=2, 按钮, permission=sys:article:add) │ └── 删除文章 (type=2, 按钮, permission=sys:article:delete) └── 分类管理 (type=1, 菜单, permission=sys:category:page) ``` ## 权限校验流程 ### 完整流程图 ```text 请求进入 AuthMiddleware │ ▼ 1. 从 JWT 获取 uid │ ▼ 2. uid === 1(超级管理员)? │ 是 → 直接放行 ▼ 3. 读取 #[Permission] 注解 │ 无注解 → 放行(该接口不需要权限) ▼ 4. 获取用户角色列表 UserRole::where('user_id', $uid)->column('role_id') │ 无角色 → return false → 403 ▼ 5. 获取角色关联的权限节点 RoleMenu → Menu::whereIn('role_id', $roleIds) .where('type', 1) // 只查权限节点 .where('is_delete', 0) .column('permission') ▼ 6. 判断权限码是否在列表中 in_array($permissionCode, $permissions) │ 不包含 → return false → 403 ▼ 7. 放行 ``` ### 校验代码 ```php // app/middleware/AuthMiddleware.php protected function checkPermission(int $uid, string $permissionCode): bool { // 超级管理员跳过校验 if ($uid === 1) { return true; } // 获取用户角色 $roleIds = UserRole::where('user_id', $uid)->column('role_id'); if (empty($roleIds)) { return false; } // 获取角色关联的权限节点 $permissions = RoleMenu::alias('rm') ->join('think_menu m', 'rm.menu_id = m.id') ->whereIn('rm.role_id', $roleIds) ->where('m.is_delete', 0) ->where('m.type', 1) // type=1 为权限节点 ->column('m.permission'); return in_array($permissionCode, $permissions); } ``` ### 错误响应 | 场景 | HTTP 状态 | code | msg | |------|----------|------|-----| | 无权限 | 200 | 403 | 无访问权限 | | 无角色 | 200 | 403 | 无访问权限 | ## 在 Controller 中使用 ### 声明权限注解 ```php use app\attribute\Log; use app\attribute\Permission; class UserController extends BaseController { // ── 查询类 ── #[Permission('sys:user:page', '用户分页')] public function page(): Json { ... } #[Permission('sys:user:detail', '用户详情')] public function detail(int $id): Json { ... } // ── 写操作类 ── #[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')] #[Permission('sys:user:add', '添加用户')] public function add(): Json { ... } #[Log('用户管理-修改记录', Log::TYPE_UPDATE, '修改用户ID:{id}')] #[Permission('sys:user:update', '修改用户')] public function update(): Json { ... } #[Log('用户管理-删除记录', Log::TYPE_DELETE, '删除用户ID:{id}')] #[Permission('sys:user:delete', '删除用户')] public function delete(int $id): Json { ... } // ── 特殊操作类 ── #[Log('用户管理-重置密码', Log::TYPE_RESET, '重置用户ID:{id}的密码')] #[Permission('sys:user:resetPwd', '重置密码')] public function resetPwd(): Json { ... } // ── 不需要权限的接口 ── // 不标注 #[Permission] 即可,所有已登录用户都可访问 public function getOptions(): Json { ... } } ``` ### 注解组合规则 ```text #[Log] + #[Permission] 组合使用: - Log:记录操作日志(无论权限校验是否通过) - Permission:校验权限(不通过返回 403) 执行顺序: AuthMiddleware 先校验权限 → 通过后 LogMiddleware 记录日志 权限校验失败时: - 返回 403,不进入 Controller - LogMiddleware 可选择是否记录(取决于配置) ``` ## 数据权限 ### 三种权限范围 ```text ┌─────────────────────────────────────────────────────────┐ │ data_scope = 1 → 全部数据 │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ A │ │ B │ │ C │ │ D │ │ E │ ← 所有部门所有人的数据 │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ ├─────────────────────────────────────────────────────────┤ │ data_scope = 2 → 本部门数据 │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到本部门数据 │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ ├─────────────────────────────────────────────────────────┤ │ data_scope = 3 → 仅本人数据 │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到自己创建的数据 │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 配置方式 在 Logic 中配置数据权限字段即可启用: ```php class ArticleLogic extends BaseTenantLogic { /** * 数据权限:归属字段名(按创建人过滤,data_scope=3 时生效) * * @var string */ protected string $dataScopeUserField = 'create_user'; /** * 数据权限:部门关联字段名(按部门过滤,data_scope=2 时生效) * * @var string */ protected string $dataScopeDeptField = 'dept_id'; } ``` ### 工作原理 ```text 查询时 applyDataScope(): 1. 获取当前用户角色 ID 2. 查询角色的 data_scope 3. scope=1 → 不过滤,返回全部数据 4. scope=2 → WHERE dept_id = 当前用户部门ID 5. scope=3 → WHERE create_user = 当前用户名 ``` :::warning 无角色用户 无角色时默认按"仅本人"(scope=3)处理。 ::: ## 权限管理接口 | 接口 | 方法 | 说明 | |------|------|------| | `/role/page` | GET | 角色分页列表 | | `/role/add` | POST | 添加角色(含菜单权限) | | `/role/update` | PUT | 修改角色(含菜单权限) | | `/role/delete/{id}` | DELETE | 删除角色 | | `/role/menus/{id}` | GET | 获取角色的菜单 ID 列表 | | `/menu/treeList` | GET | 菜单树形列表(用于权限分配) | ### 角色权限分配流程 ```text 1. 前端调用 GET /menu/treeList 获取完整菜单树 2. 前端调用 GET /role/menus/{roleId} 获取该角色已有的菜单 ID 3. 前端渲染树形 checkbox,回显已选中项 4. 用户勾选/取消菜单权限 5. 前端调用 POST /role/add 或 PUT /role/update 提交 { name, code, dataScope, menuIds: [1,2,3,...] } 6. 后端 RoleLogic::afterAdd/afterUpdate 清空并重新写入 role_menu 关联表 ``` ## 前端权限控制 ### 按钮级权限指令 ```vue ``` ### 超级管理员 用户权限列表包含 `*:*:*` 时,表示超级管理员,`v-perm`/`v-perms` 指令始终放行。 ### 菜单动态渲染 ```text 后端 GET /index/getMenus │ 返回当前用户的菜单树(已过滤无权限的菜单) ▼ 前端 asyncRouteStore │ 将菜单树转换为 Vue Router 路由 │ router.addRoute() 动态注入 ▼ 侧边栏组件 │ 遍历菜单树渲染菜单项 ▼ 用户只能看到有权限的菜单 ``` ## 最佳实践 | 实践 | 说明 | |------|------| | 权限码命名统一 | 使用 `sys:{module}:{action}` 格式 | | 最小权限原则 | 只分配必要的权限,不给多余权限 | | 角色职责单一 | 每个角色对应明确的职责范围 | | 定期审计权限 | 定期检查角色权限分配是否合理 | | 超级管理员仅一个 | uid=1 仅用于系统初始化,日常使用普通管理员账号 | | 数据权限配合功能权限 | 功能权限控制"能做什么",数据权限控制"能看到什么" |