Become a sponsor

概述
系统采用 RBAC(基于角色的访问控制)权限模型,通过"用户 → 角色 → 菜单/权限"三层关系实现细粒度的权限控制。权限控制分为两个维度:功能权限(能做什么操作)和数据权限(能看到哪些数据)。
┌─────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
│ 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 | 状态 |
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 | 重置密码 |
用户管理:
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 的父菜单下 |
系统管理 (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)请求进入 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. 放行// 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 | 无访问权限 |
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 { ... }
}#[Log] + #[Permission] 组合使用:
- Log:记录操作日志(无论权限校验是否通过)
- Permission:校验权限(不通过返回 403)
执行顺序:
AuthMiddleware 先校验权限 → 通过后 LogMiddleware 记录日志
权限校验失败时:
- 返回 403,不进入 Controller
- LogMiddleware 可选择是否记录(取决于配置)┌─────────────────────────────────────────────────────────┐
│ data_scope = 1 → 全部数据 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │ C │ │ D │ │ E │ ← 所有部门所有人的数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
├─────────────────────────────────────────────────────────┤
│ data_scope = 2 → 本部门数据 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到本部门数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
├─────────────────────────────────────────────────────────┤
│ data_scope = 3 → 仅本人数据 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │███│ │ D │ │ E │ ← 只看到自己创建的数据 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
└─────────────────────────────────────────────────────────┘在 Logic 中配置数据权限字段即可启用:
class ArticleLogic extends BaseTenantLogic
{
/**
* 数据权限:归属字段名(按创建人过滤,data_scope=3 时生效)
*
* @var string
*/
protected string $dataScopeUserField = 'create_user';
/**
* 数据权限:部门关联字段名(按部门过滤,data_scope=2 时生效)
*
* @var string
*/
protected string $dataScopeDeptField = 'dept_id';
}查询时 applyDataScope():
1. 获取当前用户角色 ID
2. 查询角色的 data_scope
3. scope=1 → 不过滤,返回全部数据
4. scope=2 → WHERE dept_id = 当前用户部门ID
5. scope=3 → WHERE create_user = 当前用户名无角色用户
无角色时默认按"仅本人"(scope=3)处理。
| 接口 | 方法 | 说明 |
|---|---|---|
/role/page | GET | 角色分页列表 |
/role/add | POST | 添加角色(含菜单权限) |
/role/update | PUT | 修改角色(含菜单权限) |
/role/delete/{id} | DELETE | 删除角色 |
/role/menus/{id} | GET | 获取角色的菜单 ID 列表 |
/menu/treeList | GET | 菜单树形列表(用于权限分配) |
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 关联表<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 指令始终放行。
后端 GET /index/getMenus
│ 返回当前用户的菜单树(已过滤无权限的菜单)
▼
前端 asyncRouteStore
│ 将菜单树转换为 Vue Router 路由
│ router.addRoute() 动态注入
▼
侧边栏组件
│ 遍历菜单树渲染菜单项
▼
用户只能看到有权限的菜单| 实践 | 说明 |
|---|---|
| 权限码命名统一 | 使用 sys:{module}:{action} 格式 |
| 最小权限原则 | 只分配必要的权限,不给多余权限 |
| 角色职责单一 | 每个角色对应明确的职责范围 |
| 定期审计权限 | 定期检查角色权限分配是否合理 |
| 超级管理员仅一个 | uid=1 仅用于系统初始化,日常使用普通管理员账号 |
| 数据权限配合功能权限 | 功能权限控制"能做什么",数据权限控制"能看到什么" |