Skip to content

数据权限

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

设计目标

目标说明
细粒度控制按组织架构维度控制数据可见范围
多角色支持用户拥有多个角色时,采用并集策略合并权限
无侵入实现基于 EF Core IQueryable 扩展,业务代码简洁
灵活配置支持全部、部门层级、个人、自定义等多种范围

数据范围类型

类型说明
1全部数据可访问系统所有数据
2本部门及子部门可访问用户所属部门及其下级部门的数据
3本部门仅可访问用户所属部门的数据
4本人仅可访问自己创建的数据
5自定义可访问指定部门的数据(需配置部门列表)

核心组件

接口定义

csharp
public interface IDataPermissionService
{
    /// <summary>
    /// 基于当前用户的数据范围追加过滤条件
    /// </summary>
    IQueryable<TEntity> Apply<TEntity>(
        IQueryable<TEntity> query,
        Expression<Func<TEntity, long>> deptIdSelector,
        Expression<Func<TEntity, long>> userIdSelector);
}

核心处理器

DataPermissionService 实现数据权限过滤的核心逻辑:

csharp
public IQueryable<TEntity> Apply(
    IQueryable<TEntity> query,
    Expression<Func<TEntity, long>> deptIdSelector,
    Expression<Func<TEntity, long>> userIdSelector)
{
    // 1. 超级管理员跳过过滤
    if (_currentUser.IsRoot)
        return query;

    var dataScopes = _currentUser.DataScopes;
    
    // 2. 没有数据权限配置,默认只能查看本人数据
    if (dataScopes == null || dataScopes.Count == 0)
        return query.Where(BuildEquals(userIdSelector, userId.Value));

    // 3. 如果任一角色是 All,则跳过数据权限过滤
    if (HasAllDataScope(dataScopes))
        return query;

    // 4. 多角色数据权限合并(并集策略)
    return ApplyWithDataScopes(query, deptIdSelector, userIdSelector, dataScopes);
}

多角色权限合并策略

当用户拥有多个角色时,采用并集(OR)策略合并数据权限:

合并规则

  1. ALL优先规则:任一角色拥有"全部数据"权限时,跳过所有过滤
  2. OR连接规则:各角色的过滤条件通过 OR 连接,取并集
  3. 表达式合并:使用 Expression.OrElse 合并多个表达式

实现代码

csharp
private IQueryable<TEntity> ApplyWithDataScopes<TEntity>(
    IQueryable<TEntity> query,
    Expression<Func<TEntity, long>> deptIdSelector,
    Expression<Func<TEntity, long>> userIdSelector,
    IReadOnlyList<RoleDataScope> dataScopes)
{
    Expression<Func<TEntity, bool>>? unionExpression = null;

    foreach (var scope in dataScopes)
    {
        var roleExpression = BuildRoleDataScopeExpression(
            deptIdSelector, userIdSelector, scope, userId, deptId);

        if (roleExpression != null)
        {
            // OR连接各角色条件
            unionExpression = unionExpression == null
                ? roleExpression
                : CombineWithOr(unionExpression, roleExpression);
        }
    }

    return unionExpression == null
        ? query.Where(e => false)
        : query.Where(unionExpression);
}

// 使用 OR 连接两个表达式
private static Expression<Func<TEntity, bool>> CombineWithOr<TEntity>(
    Expression<Func<TEntity, bool>> left,
    Expression<Func<TEntity, bool>> right)
{
    var parameter = left.Parameters[0];
    var visitor = new ReplaceParameterVisitor(right.Parameters[0], parameter);
    var rightBody = visitor.Visit(right.Body);
    var orExpression = Expression.OrElse(left.Body, rightBody);
    return Expression.Lambda<Func<TEntity, bool>>(orExpression, parameter);
}

自定义部门权限

数据库设计

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 '角色部门关联表';

登录时构建数据权限

csharp
// AuthService.GenerateTokenAsync
private async Task<AuthenticationTokenDto> GenerateTokenAsync(long userId, CancellationToken cancellationToken)
{
    // ... 用户验证 ...

    // 查询用户的所有角色及其数据权限
    var roles = await (from ur in _dbContext.SysUserRoles
                       join r in _dbContext.SysRoles on ur.RoleId equals r.Id
                       where ur.UserId == userId && !r.IsDeleted && r.Status == 1
                       select new { r.Id, r.Code, r.DataScope })
                      .ToListAsync(cancellationToken);

    // 构建数据权限列表
    var dataScopes = new List<RoleDataScope>();
    foreach (var role in roles)
    {
        var roleDataScope = new RoleDataScope
        {
            RoleCode = role.Code ?? string.Empty,
            DataScope = role.DataScope ?? 4
        };

        // 如果是自定义部门权限,查询该角色的自定义部门列表
        if (role.DataScope == 5 && role.Id != 0)
        {
            roleDataScope.CustomDeptIds = await _dbContext.SysRoleDepts
                .Where(rd => rd.RoleId == role.Id)
                .Select(rd => rd.DeptId)
                .ToListAsync(cancellationToken);
        }

        dataScopes.Add(roleDataScope);
    }

    // 生成 JWT Token
    var subject = new JwtTokenManager.AuthTokenSubject(
        UserId: user.Id,
        DeptId: user.DeptId ?? 0,
        DataScopes: dataScopes,
        Username: user.Username ?? string.Empty,
        Authorities: authorities
    );

    return _tokenManager.GenerateToken(subject);
}

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]
    }
  ]
}

CurrentUser 解析

csharp
public sealed class CurrentUser : ICurrentUser
{
    public IReadOnlyList<RoleDataScope>? DataScopes
    {
        get
        {
            var dataScopesClaim = Principal?.FindFirst(JwtClaimConstants.DataScopes);
            if (string.IsNullOrWhiteSpace(dataScopesClaim?.Value))
                return null;

            try
            {
                var scopes = JsonSerializer.Deserialize<List<RoleDataScope>>(dataScopesClaim.Value);
                return scopes?.AsReadOnly();
            }
            catch
            {
                return null;
            }
        }
    }
}

使用指南

基本用法

csharp
public class SystemUserService : ISystemUserService
{
    private readonly IDataPermissionService _dataPermissionService;

    public async Task<PageResult<UserPageVo>> GetUserPageAsync(UserQuery query, CancellationToken cancellationToken)
    {
        var users = _dbContext.SysUsers.AsNoTracking().Where(u => !u.IsDeleted);

        // 应用数据权限过滤
        users = _dataPermissionService.Apply(users, u => u.DeptId ?? 0, u => u.Id);

        // 其他查询条件...

        return await PageResult<UserPageVo>.CreateAsync(users, query.PageNum, query.PageSize);
    }
}

过滤条件说明

数据范围过滤逻辑
All不添加任何过滤条件
DeptAndSub基于部门树路径 tree_path 过滤,使用 EF.Functions.Like 匹配
DeptdeptId == currentUser.DeptId
SelfuserId == currentUser.UserId
CustomdeptId IN (customDeptIds) 子查询

部门及子部门实现

csharp
private Expression<Func<TEntity, bool>> BuildDeptAndSubExpression<TEntity>(
    Expression<Func<TEntity, long>> deptIdSelector,
    long deptId)
{
    var deptIdStr = deptId.ToString();
    var likeMiddle = "%," + deptIdStr + ",%";
    var likeTail = "%," + deptIdStr;
    var likeHead = deptIdStr + ",%";

    var deptIdsQuery = _dbContext.SysDepts
        .AsNoTracking()
        .Where(d => !d.IsDeleted && (
            d.Id == deptId ||
            d.TreePath == deptIdStr ||
            EF.Functions.Like(d.TreePath, likeMiddle) ||
            EF.Functions.Like(d.TreePath, likeHead) ||
            EF.Functions.Like(d.TreePath, likeTail)))
        .Select(d => d.Id);

    return BuildContains(deptIdsQuery, deptIdSelector);
}

相关文件

文件路径说明
Youlai.Application/Common/Security/DataScope.cs数据权限枚举定义
Youlai.Application/Common/Security/RoleDataScope.cs角色数据权限模型
Youlai.Application/Common/Security/ICurrentUser.cs当前用户接口
Youlai.Api/Security/CurrentUser.cs当前用户实现
Youlai.Infrastructure/Services/DataPermissionService.cs核心处理器
Youlai.Infrastructure/Services/AuthService.cs登录时构建数据权限
Youlai.Infrastructure/Services/JwtTokenManager.csJWT 生成与解析

基于 MIT 许可发布