权限系统设计
CondorAdmin 采用基于 RBAC(Role-Based Access Control) 的权限模型,提供灵活的用户、角色、权限三级管理,支持菜单权限、按钮权限和数据权限。
权限模型
RBAC 三级模型
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户 │ N:M │ 角色 │ N:M │ 权限 │
│ (Admin) │─────→│ (Role) │─────→│ (Menu) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ ├─ 菜单权限
│ │ ├─ 按钮权限
│ │ └─ API 路由
│ │
└─────────────────────┘
数据权限
(dataLimit)关系说明:
- 用户 ↔ 角色:多对多关系(一个用户可以有多个角色)
- 角色 ↔ 权限:多对多关系(一个角色可以有多个权限)
- 数据权限:基于角色或用户的数据过滤规则
数据库设计
核心表结构
sql
-- 用户表
system_admin
├── id 主键
├── username 用户名(唯一)
├── password 密码(bcrypt)
├── nickname 昵称
├── role_ids 角色ID列表(JSON: [1,2,3])
├── status 状态(0=禁用 1=启用)
└── ...
-- 角色表
system_role
├── id 主键
├── name 角色名称
├── rules 权限ID列表(JSON: [1,2,3])
├── group_id 角色分组ID
└── ...
-- 权限表(菜单表)
system_menu_rule
├── id 主键
├── pid 父级ID(树形结构)
├── type 类型(menu=菜单 button=按钮)
├── title 标题
├── path 路由路径
├── name 路由名称
├── component 组件路径
├── icon 图标
├── is_hide 是否隐藏
├── is_keepalive 是否缓存
├── order 排序
└── ...
-- 角色分组表
system_role_group
├── id 主键
├── pid 父级ID
├── name 分组名称
├── rules 继承的权限ID
└── ...权限类型
1. 菜单权限
控制用户可以访问哪些页面(路由)。
菜单树示例:
系统管理
├── 管理员管理 (/system/admin)
│ ├── 查看列表 (button: admin:index)
│ ├── 添加 (button: admin:add)
│ ├── 编辑 (button: admin:edit)
│ └── 删除 (button: admin:del)
├── 角色管理 (/system/role)
└── 菜单管理 (/system/menu)数据库存储:
json
{
"id": 1,
"type": "menu",
"title": "管理员管理",
"path": "/system/admin",
"component": "view.system_admin",
"icon": "mdi:account-supervisor"
}2. 按钮权限
控制页面内的操作按钮(增删改查)。
按钮权限标识:
| 按钮 | 权限标识 | 说明 |
|---|---|---|
| 添加 | admin:add | 添加管理员 |
| 编辑 | admin:edit | 编辑管理员 |
| 删除 | admin:del | 删除管理员 |
| 导出 | admin:export | 导出数据 |
前端权限检查:
vue
<template>
<!-- 使用 v-if 控制按钮显示 -->
<n-button v-if="hasPermission('admin:add')" @click="handleAdd">
添加
</n-button>
<!-- 使用 CondorAuth 组件 -->
<CondorAuth auth="admin:add">
<n-button @click="handleAdd">添加</n-button>
</CondorAuth>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store';
const authStore = useAuthStore();
// 权限检查函数
const hasPermission = (auth: string) => {
return authStore.hasPermission(auth);
};
</script>3. API 路由权限
控制后端接口的访问权限。
路由-权限映射:
/api/condoradmin/admin/index → admin:index
/api/condoradmin/admin/add → admin:add
/api/condoradmin/admin/edit → admin:edit
/api/condoradmin/admin/del → admin:del后端权限验证:
php
// AuthPermission 中间件自动验证
$auth->check($request->path(), $auth->id);免权限控制:
php
class AdminController extends Backend
{
// 无需登录的方法
protected $noNeedLogin = ['login', 'captcha'];
// 无需鉴权的方法(但需登录)
protected $noNeedRight = ['profile', 'updatePassword'];
}4. 数据权限
控制用户只能看到属于自己范围的数据。
数据权限级别:
| 级别 | 值 | 说明 | 应用场景 |
|---|---|---|---|
| 全部 | false | 不限制,查看所有数据 | 超级管理员 |
| 部门 | 'auth' | 查看本人及下级的数据 | 部门主管 |
| 个人 | 'personal' | 只查看本人的数据 | 普通员工 |
后端配置:
php
class OrderController extends Backend
{
protected $model = Order::class;
// 启用数据权限
protected $dataLimit = 'auth'; // 或 'personal'
// 数据权限字段(关联的用户ID字段)
protected $dataLimitField = 'admin_id';
}自动过滤逻辑:
php
// Backend 基类的 getDataLimitAdminIds() 方法
protected function getDataLimitAdminIds()
{
// 超级管理员不受限制
if ($this->auth->isSuperAdmin()) {
return [];
}
// 根据 dataLimit 类型获取可见的管理员ID
if ($this->dataLimit === 'auth') {
// 获取本人及所有下级的ID
return $this->auth->getChildrenAdminIds(true);
}
if ($this->dataLimit === 'personal') {
// 只返回本人ID
return [$this->auth->id];
}
return [];
}
// 查询时自动应用
$query->whereIn($this->dataLimitField, $childrenAdminIds);权限验证流程
前端权限验证
1. 用户登录成功
↓
2. 获取用户信息(包含 roles + buttons)
↓
3. Store 存储权限列表
- userInfo.roles: ['admin', 'editor']
- userInfo.buttons: ['admin:add', 'admin:edit']
↓
4. 路由守卫检查菜单权限
- 根据用户角色过滤路由
- 无权限路由重定向到 403
↓
5. 组件内检查按钮权限
- hasPermission('admin:add')
- 返回 true/false 控制按钮显示路由守卫代码:
typescript
// src/router/guard/route.ts
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
// 未登录
if (!authStore.isLogin) {
if (to.meta.constant) {
next();
} else {
next({ name: 'login' });
}
return;
}
// 已登录,初始化路由
if (!routeStore.isInitAuthRoute) {
await routeStore.initAuthRoute();
}
// 检查路由权限
if (to.name === 'not-found') {
next();
return;
}
next();
});后端权限验证
1. 请求进入 AuthToken 中间件
↓
2. 验证 Token 有效性(Redis)
- 无效 → 返回 401
- 有效 → 继续
↓
3. 请求进入 AuthPermission 中间件
↓
4. 检查控制器的 $noNeedLogin 和 $noNeedRight
- 在白名单 → 跳过权限验证
- 不在白名单 → 继续
↓
5. 调用 Auth::check() 验证权限
- 根据请求路径查找对应的权限ID
- 判断用户是否拥有该权限
↓
6. 权限验证结果
- 有权限 → 继续执行
- 无权限 → 返回 403AuthPermission 中间件代码:
php
public function process(Request $request, callable $handler): Response
{
$controller = $request->controller;
$action = $request->action;
// 获取控制器鉴权配置
$class = new \ReflectionClass($controller);
$properties = $class->getDefaultProperties();
$noNeedLogin = $properties['noNeedLogin'] ?? [];
if (in_array($action, $noNeedLogin)) {
return $handler($request);
}
$noNeedRight = $properties['noNeedRight'] ?? [];
$auth = new Auth();
$auth->initUser();
if (in_array($action, $noNeedRight)) {
Context::set('auth', $auth);
return $handler($request);
}
// 检查权限
if ($auth->check($request->path(), $auth->id) !== true) {
return json(['code' => 403, 'msg' => trans('common.access.denied')]);
}
Context::set('auth', $auth);
return $handler($request);
}超级管理员
特殊标识
超级管理员拥有所有权限,无需配置。
判断逻辑:
php
// Auth 类
public function isSuperAdmin(): bool
{
return in_array('*', $this->roles);
}特权:
- 绕过所有权限检查
- 绕过数据权限限制
- 可以修改所有字段(批量更新时)
php
// Backend::multi() 方法
$values = $this->auth->isSuperAdmin()
? $values
: array_intersect_key($values, array_flip($this->multiFields));超级管理员配置
数据库中设置:
sql
UPDATE system_admin SET role_ids = '["*"]' WHERE id = 1;或在角色表中设置:
sql
UPDATE system_role SET rules = '["*"]' WHERE id = 1;权限缓存
Redis 缓存结构
# Token 缓存(过期自动删除)
admin:token:{token} = {admin_id}
TTL: 7200 秒(2小时)
# 用户权限缓存
admin:rules:{admin_id} = ["admin:add", "admin:edit", ...]
TTL: 3600 秒(1小时)缓存更新时机
- 用户登录:写入 Token
- 角色/权限变更:清除相关用户的权限缓存
- 用户退出:删除 Token
php
// 清除权限缓存
Redis::del("admin:rules:{$adminId}");使用示例
示例1:配置角色权限
php
// 创建角色
$role = new SystemRole();
$role->name = '编辑';
$role->rules = json_encode([10, 11, 12, 13]); // 权限ID
$role->save();
// 分配角色给用户
$admin = SystemAdmin::find(5);
$admin->role_ids = json_encode([2]); // 角色ID
$admin->save();示例2:前端权限控制
vue
<template>
<div>
<!-- 菜单权限:路由守卫自动处理 -->
<!-- 按钮权限:使用 v-if -->
<n-button v-if="hasPermission('admin:add')" @click="handleAdd">
添加
</n-button>
<!-- 按钮权限:使用 CondorAuth 组件 -->
<CondorAuth auth="admin:edit">
<n-button @click="handleEdit">编辑</n-button>
</CondorAuth>
<!-- 多权限或判断(任一满足即可) -->
<CondorAuth :auth="['admin:del', 'admin:destroy']">
<n-button @click="handleDelete">删除</n-button>
</CondorAuth>
<!-- 多权限且判断(全部满足才显示) -->
<n-button v-if="hasAllPermissions(['admin:del', 'superuser'])" @click="handleForceDelete">
强制删除
</n-button>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store';
const authStore = useAuthStore();
const hasPermission = (auth: string) => {
return authStore.hasPermission(auth);
};
const hasAllPermissions = (auths: string[]) => {
return auths.every(auth => authStore.hasPermission(auth));
};
</script>示例3:后端数据权限
php
class OrderController extends Backend
{
protected $model = Order::class;
// 部门数据权限
protected $dataLimit = 'auth';
protected $dataLimitField = 'admin_id';
// 查询时自动过滤
// 普通管理员只能看到自己和下级的订单
// 超级管理员可以看到所有订单
}示例4:自定义权限检查
php
// Controller 中
public function customAction(Request $request)
{
// 检查特定权限
if (!$this->auth->check('custom:permission', $this->auth->id)) {
return $this->fail('权限不足');
}
// 检查是否为超级管理员
if (!$this->auth->isSuperAdmin()) {
return $this->fail('仅超级管理员可操作');
}
// 业务逻辑
}最佳实践
1. 权限命名规范
模块:操作
示例:
admin:index # 管理员-列表
admin:add # 管理员-添加
admin:edit # 管理员-编辑
admin:del # 管理员-删除
order:export # 订单-导出2. 角色设计原则
- 按职能划分:管理员、编辑、运营、客服
- 最小权限原则:只分配必需的权限
- 继承机制:通过角色分组实现权限继承
3. 数据权限应用场景
- 订单管理:销售只能看自己的订单
- 客户管理:客服只能看自己负责的客户
- 报表统计:按数据权限统计个人/部门业绩
4. 安全建议
- 定期审计:定期检查用户权限分配
- 权限回收:员工离职及时回收权限
- 日志记录:记录敏感操作日志
- 双因素认证:高权限账号启用 2FA
常见问题
Q1: 如何添加新权限?
后端:
- 在
system_menu_rule表添加权限记录 - 清除权限缓存
前端: 无需修改代码,权限从后端动态获取
Q2: 如何调试权限问题?
php
// 打印用户权限列表
Log::info('User Permissions', $this->auth->getRuleList());
// 检查特定权限
if (!$this->auth->check('admin:add', $this->auth->id)) {
Log::warning('Permission Denied', [
'user_id' => $this->auth->id,
'permission' => 'admin:add'
]);
}Q3: 数据权限如何配置?
在控制器中设置 $dataLimit 和 $dataLimitField:
php
protected $dataLimit = 'auth'; // 或 'personal'
protected $dataLimitField = 'admin_id'; // 数据所属用户字段确保数据表中有对应字段,并在添加数据时自动填充:
php
protected $createdByField = 'admin_id';