签入版本
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
# 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=权限节点 |
|
||||
| `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
|
||||
<template>
|
||||
<!-- 单权限:拥有 sys:user:add 才显示 -->
|
||||
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
|
||||
|
||||
<!-- 多权限:拥有任一权限即显示 -->
|
||||
<el-button v-perms="['sys:user:add', 'sys:user:update']">操作</el-button>
|
||||
|
||||
<!-- 无权限的按钮直接从 DOM 移除,而非仅隐藏 -->
|
||||
</template>
|
||||
```
|
||||
|
||||
### 超级管理员
|
||||
|
||||
用户权限列表包含 `*:*:*` 时,表示超级管理员,`v-perm`/`v-perms` 指令始终放行。
|
||||
|
||||
### 菜单动态渲染
|
||||
|
||||
```text
|
||||
后端 GET /index/getMenus
|
||||
│ 返回当前用户的菜单树(已过滤无权限的菜单)
|
||||
▼
|
||||
前端 asyncRouteStore
|
||||
│ 将菜单树转换为 Vue Router 路由
|
||||
│ router.addRoute() 动态注入
|
||||
▼
|
||||
侧边栏组件
|
||||
│ 遍历菜单树渲染菜单项
|
||||
▼
|
||||
用户只能看到有权限的菜单
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
| 实践 | 说明 |
|
||||
|------|------|
|
||||
| 权限码命名统一 | 使用 `sys:{module}:{action}` 格式 |
|
||||
| 最小权限原则 | 只分配必要的权限,不给多余权限 |
|
||||
| 角色职责单一 | 每个角色对应明确的职责范围 |
|
||||
| 定期审计权限 | 定期检查角色权限分配是否合理 |
|
||||
| 超级管理员仅一个 | uid=1 仅用于系统初始化,日常使用普通管理员账号 |
|
||||
| 数据权限配合功能权限 | 功能权限控制"能做什么",数据权限控制"能看到什么" |
|
||||
Reference in New Issue
Block a user