Skip to content

数据权限

数据权限是一种行级安全控制机制,用于限制用户只能访问其权限范围内的数据行。与功能权限(控制用户能执行哪些操作)不同,数据权限控制用户能看到哪些数据。

设计目标

目标说明
细粒度控制按组织架构维度控制数据可见范围
多角色支持用户拥有多个角色时,采用并集策略合并权限
无侵入实现封装 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)策略合并数据权限:

合并规则

  1. ALL优先规则:任一角色拥有"全部数据"权限时,跳过所有过滤
  2. OR连接规则:各角色的过滤条件通过 OR 连接,取并集
  3. 条件隔离规则:各角色的条件用括号包裹,避免逻辑歧义

实现代码

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 匹配
DeptdeptId = currentUser.DeptId
SelfuserId = currentUser.UserId
CustomdeptId 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.phpJWT 声明常量
app/common/security/JwtTokenManager.phpJWT 生成与解析
app/middleware/DataScopeMiddleware.php数据范围中间件
app/service/DataPermissionService.php核心处理器
app/service/UserService.php用户服务(应用权限过滤)
app/service/DeptService.php部门服务(应用权限过滤)
app/service/NoticeService.php通知服务(应用权限过滤)

基于 MIT 许可发布