数据权限
数据权限是一种行级安全控制机制,用于限制用户只能访问其权限范围内的数据行。与功能权限(控制用户能执行哪些操作)不同,数据权限控制用户能看到哪些数据。
设计目标
| 目标 | 说明 |
|---|---|
| 细粒度控制 | 按组织架构维度控制数据可见范围 |
| 多角色支持 | 用户拥有多个角色时,采用并集策略合并权限 |
| 无侵入实现 | 封装 DataPermissionService,业务代码简洁 |
| 灵活配置 | 支持全部、部门层级、个人、自定义等多种范围 |
数据范围类型
| 值 | 类型 | 说明 |
|---|---|---|
1 | 全部数据 | 可访问系统所有数据 |
2 | 本部门及子部门 | 可访问用户所属部门及其下级部门的数据 |
3 | 本部门 | 仅可访问用户所属部门的数据 |
4 | 本人 | 仅可访问自己创建的数据 |
5 | 自定义 | 可访问指定部门的数据(需配置部门列表) |
核心组件
服务定义
php
class DataPermissionService
{
/**
* 应用数据权限过滤
*
* @param object $query 查询构建器
* @param string $deptIdColumn 部门ID字段名(如 'u.dept_id')
* @param string $userIdColumn 用户ID字段名(如 'u.id' 或 'u.create_by')
* @param array $authUser 当前用户信息
* @return object 过滤后的查询构建器
*/
public function apply(object $query, string $deptIdColumn, string $userIdColumn, array $authUser): object;
}核心处理器
DataPermissionService 实现数据权限过滤的核心逻辑:
php
public function apply(object $query, string $deptIdColumn, string $userIdColumn, array $authUser): object
{
// 1. 超级管理员跳过过滤
if ($this->isRoot($authUser)) {
return $query;
}
$dataScopes = $authUser['dataScopes'] ?? [];
// 2. 没有数据权限配置,默认只能查看本人数据
if (empty($dataScopes)) {
return $query->where($userIdColumn, $userId);
}
// 3. 如果任一角色是 ALL,则跳过数据权限过滤
if ($this->hasAllDataScope($dataScopes)) {
return $query;
}
// 4. 多角色数据权限合并(并集策略)
return $this->applyWithDataScopes($query, $deptIdColumn, $userIdColumn, $dataScopes, $userId, $deptId);
}多角色权限合并策略
当用户拥有多个角色时,采用并集(OR)策略合并数据权限:
合并规则
- ALL优先规则:任一角色拥有"全部数据"权限时,跳过所有过滤
- OR连接规则:各角色的过滤条件通过
OR连接,取并集 - 条件隔离规则:各角色的条件用括号包裹,避免逻辑歧义
实现代码
php
private function applyWithDataScopes(
object $query,
string $deptIdColumn,
string $userIdColumn,
array $dataScopes,
int $userId,
?int $deptId
): object {
$conditions = [];
$bindings = [];
foreach ($dataScopes as $scope) {
$condition = $this->buildRoleCondition(
$scope, $deptIdColumn, $userIdColumn, $userId, $deptId, $bindings);
if ($condition !== null) {
$conditions[] = $condition;
}
}
// 使用 OR 连接各角色条件(并集)
$orCondition = '(' . implode(' OR ', $conditions) . ')';
return $query->whereRaw($orCondition, $bindings);
}自定义部门权限
数据库设计
sql
-- 角色表增加数据权限字段
ALTER TABLE sys_role ADD COLUMN data_scope TINYINT DEFAULT 1
COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)';
-- 角色部门关联表(自定义权限使用)
CREATE TABLE sys_role_dept (
role_id BIGINT NOT NULL COMMENT '角色ID',
dept_id BIGINT NOT NULL COMMENT '部门ID',
PRIMARY KEY (role_id, dept_id)
) COMMENT '角色部门关联表';登录时构建数据权限
php
// JwtTokenManager::buildDataScopes
private function buildDataScopes(int $userId): array
{
$roles = Db::name('sys_user_role')
->alias('ur')
->join('sys_role r', 'ur.role_id = r.id')
->where('ur.user_id', $userId)
->where('r.is_deleted', 0)
->where('r.status', 1)
->field('r.id,r.code,r.data_scope')
->select()
->toArray();
$dataScopes = [];
foreach ($roles as $role) {
$customDeptIds = null;
// 如果是自定义部门权限,查询该角色的自定义部门列表
if ($dataScope === 5 && !empty($role['id'])) {
$customDeptIds = Db::name('sys_role_dept')
->where('role_id', $role['id'])
->column('dept_id');
}
$dataScopes[] = [
'roleCode' => $role['code'] ?? '',
'dataScope' => $dataScope,
'customDeptIds' => $customDeptIds,
];
}
return $dataScopes;
}JWT Token 存储
多角色数据权限存储在JWT Token中,避免每次查询数据库:
json
{
"userId": 1,
"deptId": 10,
"authorities": ["ROLE_ADMIN", "ROLE_USER"],
"dataScopes": [
{
"roleCode": "ADMIN",
"dataScope": 1,
"customDeptIds": null
},
{
"roleCode": "MANAGER",
"dataScope": 5,
"customDeptIds": [20, 30, 40]
}
]
}中间件解析
php
// DataScopeMiddleware
public function handle($request, \Closure $next)
{
$authUser = $request->getAuthUser();
// dataScopes 已经在 JWT 中解析
if (!isset($authUser['dataScopes'])) {
$authUser['dataScopes'] = [];
}
// 提取 roles 列表便于判断 ROOT 角色
$authUser['roles'] = $this->extractRoles($authUser['authorities']);
$request->setAuthUser($authUser);
return $next($request);
}使用指南
基本用法
php
public function getUserPage(array $queryParams, ?array $authUser = null): array
{
$q = Db::name('sys_user')
->alias('u')
->where('u.is_deleted', 0);
// 应用数据权限过滤
if (is_array($authUser)) {
$dataPermissionService = new DataPermissionService();
$q = $dataPermissionService->apply($q, 'u.dept_id', 'u.id', $authUser);
}
// 其他查询条件...
return $rows;
}过滤条件说明
| 数据范围 | 过滤逻辑 |
|---|---|
All | 不添加任何过滤条件 |
DeptAndSub | 基于 sys_dept.tree_path 使用 FIND_IN_SET 匹配 |
Dept | deptId = currentUser.DeptId |
Self | userId = currentUser.UserId |
Custom | deptId IN (customDeptIds) |
部门及子部门实现
php
private function getDeptAndSubIds(int $deptId): array
{
$deptIdStr = (string) $deptId;
return Db::name('sys_dept')
->where('is_deleted', 0)
->where(function ($query) use ($deptId, $deptIdStr) {
$query->where('id', $deptId)
->whereOr('tree_path', $deptIdStr)
->whereOrRaw("FIND_IN_SET(?, tree_path)", [$deptIdStr]);
})
->column('id');
}相关文件
| 文件路径 | 说明 |
|---|---|
app/common/enums/DataScopeEnum.php | 数据权限枚举定义 |
app/common/security/UserSession.php | 角色数据权限模型 |
app/common/security/JwtClaimConstants.php | JWT 声明常量 |
app/common/security/JwtTokenManager.php | JWT 生成与解析 |
app/middleware/DataScopeMiddleware.php | 数据范围中间件 |
app/service/DataPermissionService.php | 核心处理器 |
app/service/UserService.php | 用户服务(应用权限过滤) |
app/service/DeptService.php | 部门服务(应用权限过滤) |
app/service/NoticeService.php | 通知服务(应用权限过滤) |
