Skip to content

5.4 权限管理(RBAC) ​

概述

系统采用 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(角色表):

字段类型说明
idINT主键
tenant_idINT租户ID:0=平台级
nameVARCHAR(50)角色名称
codeVARCHAR(50)角色编码
data_scopeTINYINT数据权限范围:1=全部 2=本部门 3=仅本人
statusTINYINT状态
remarkVARCHAR(500)备注
sortINT排序

think_menu(菜单/权限表):

字段类型说明
idINT主键
parent_idINT父级 ID(0=顶级)
nameVARCHAR(50)菜单名称
permissionVARCHAR(100)权限标识(如 sys:user:add)
typeTINYINT类型:0=目录 1=菜单 2=按钮
pathVARCHAR(200)路由路径
iconVARCHAR(100)图标
sortINT排序
statusTINYINT状态

权限码格式 ​

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}:importExcel 导入
导出sys:{module}:exportExcel 导出
重置密码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 状态codemsg
无权限200403无访问权限
无角色200403无访问权限

在 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 = 当前用户名

无角色用户

无角色时默认按"仅本人"(scope=3)处理。

权限管理接口 ​

接口方法说明
/role/pageGET角色分页列表
/role/addPOST添加角色(含菜单权限)
/role/updatePUT修改角色(含菜单权限)
/role/delete/{id}DELETE删除角色
/role/menus/{id}GET获取角色的菜单 ID 列表
/menu/treeListGET菜单树形列表(用于权限分配)

角色权限分配流程 ​

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 仅用于系统初始化,日常使用普通管理员账号
数据权限配合功能权限功能权限控制"能做什么",数据权限控制"能看到什么"

小蚂蚁云团队 · 提供技术支持