数据权限
数据权限是一种行级安全控制机制,用于限制用户只能访问其权限范围内的数据行。与功能权限(控制用户能执行哪些操作)不同,数据权限控制用户能看到哪些数据。
设计目标
| 目标 | 说明 |
|---|---|
| 细粒度控制 | 按组织架构维度控制数据可见范围 |
| 多角色支持 | 用户拥有多个角色时,采用并集策略合并权限 |
| 无侵入实现 | 基于 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)策略合并数据权限:
合并规则
- ALL优先规则:任一角色拥有"全部数据"权限时,跳过所有过滤
- OR连接规则:各角色的过滤条件通过
OR连接,取并集 - 表达式合并:使用
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 匹配 |
Dept | deptId == currentUser.DeptId |
Self | userId == currentUser.UserId |
Custom | deptId 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.cs | JWT 生成与解析 |
