数据过滤体系
数据过滤体系
一、配置与数据权限范围
在 系统管理 → 角色管理 → 修改角色 → 数据权限 选择范围。
DataPermiEnum — 角色可选权限
| 枚举值 | 数值 | 含义 |
|---|---|---|
None | 0 | 无数据权限 |
All | 1 | 全部数据权限 |
CUSTOM | 2 | 自定义数据权限 |
DEPT | 3 | 本部门数据 |
DEPT_CHILD | 4 | 本部门及以下数据 |
SELF | 5 | 仅本人数据 |
定义位置:
ZR.ServiceCore/SqlSugar/DataPermi.cs(DataPermiEnum)
MergedScopeType — 登录时合并后的权限等级
一个用户可能拥有多个角色,每个角色配置了不同的 DataPermiEnum。登录时取最宽松的一档合并,存入 LoginUser.ScopeType:
| MergedScopeType | 数值 | 含义 |
|---|---|---|
None | 0 | 无角色(非 HTTP 场景回退到此值) |
Self | 1 | 仅本人 |
Dept | 2 | 本部门 |
DeptList | 3 | 指定部门列表(DEPT_CHILD ∪ CUSTOM 并集) |
All | 4 | 全部数据(管理员 或 DataScope=All) |
定义位置:
Infrastructure/Model/LoginUser.cs(MergedScopeType)
DataScopeExtensions.IsAllScope()即判断ScopeType == All,满足时所有过滤器自动跳过。
注意
测试数据权限不要使用 admin 账号或 admin 权限字符,否则 IsAllScope() 始终为 true,看不到过滤效果。
二、两种机制与决策
2.1 机制速览
| 机制 | 过滤依据 | 适用表 | 实现方式 | 负责模块 |
|---|---|---|---|---|
| 租户隔离 | TenantId | 主库共享表 | SQL Sugar 全局 QueryFilter(AddTableFilter) | TenantFilter |
| 角色数据权限 | DataScope | 租户库表 | ApplyScope 扩展 + RegisterScopeFilter 注册表 | DataScopeExtensions |
2.2 新增表快速决策
| 条件 | 选择 |
|---|---|
| 表存主库、不同租户共享 | 租户隔离:实体实现 IMainDbEntity、加 TenantId 列、TenantFilter 注册 |
| 表存租户库、按角色权限过滤 | 角色数据权限:添加 UserId/DeptId 列、ApplyScope 扩展方法 + RegisterScopeFilter 注册 |
| 表存租户库、所有角色都能看全部 | 不需要过滤 |
| 两种都需要(主库实体 + 角色权限) | 两边都加(同时加 TenantFilter + DataScopeExtensions) |
2.3 受保护的实体(内置)
以下实体默认启用数据权限过滤,通过 BaseRepository.Queryable() 自动生效:
| 实体 | 过滤规则 |
|---|---|
SysUser | 仅自己 + 部门范围内的用户 |
SysDept | 仅部门范围内的部门 |
SysRole | 仅用户拥有的角色 |
SysLogininfor | 仅自己的登录日志 |
UserOnlineLog | 仅自己的在线日志 |
当用户拥有
All权限时以上过滤器全部自动跳过。
三、租户隔离(IMainDbEntity + TenantFilter)
适用场景:表存主库,不同租户的数据按 TenantId 互相隔离。
3.1 IMainDbEntity
实现 IMainDbEntity 接口的实体始终路由到主库 MainDb 连接,无论当前请求属于哪个租户:
// 文件: ZR.Model/MainDbAttribute.cs
public interface IMainDbEntity { }
// 示例:SysTenantPlanMenu 始终操作主库
[SugarTable("sys_tenant_plan_menu", "套餐菜单配置表")]
[Tenant("0")]
public class SysTenantPlanMenu : SysBase, IMainDbEntity
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public long Id { get; set; }
public string PlanCode { get; set; }
public long MenuId { get; set; }
}
已内置的 IMainDbEntity 实体:SysDictData、SysDictType、SysTenant、SysTenantPlan、SysTenantPlanBinding、SysTenantPlanMenu、SysMenu、SqlDiffLog。
3.2 新增实体接入步骤
以新增 Order 表为例:
Step 1 — 实体定义
// ZR.Model/YourModule/Order.cs
namespace ZR.Model.YourModule
{
[SugarTable("order")]
public class Order : IMainDbEntity // ← 标记主库实体
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public long Id { get; set; }
/// <summary>租户ID(必加)</summary>
[SugarColumn(Length = 64, IsNullable = true)]
public string TenantId { get; set; }
// ... 其他业务字段
}
}
Step 2 — 添加 TenantFilter 表达式
在 ZR.ServiceCore/SqlSugar/TenantFilter.cs 中新增过滤方法(参考已有的 SysUserMsgTenantFilter / SysFileTenantFilter / SysFileGroupTenantFilter):
// 主租户兼容 TenantId==null 或 TenantId==MainDbConfigId 的历史数据
public static Expression<Func<Order, bool>> OrderTenantFilter() => it =>
it.TenantId == App.GetCurrentTenantId()
|| (App.GetCurrentTenantId() == App.MainDbConfigId
&& (it.TenantId == null || it.TenantId == App.MainDbConfigId));
Step 3 — SqlsugarSetup 注册
在 ZR.ServiceCore/SqlSugar/SqlsugarSetup.cs 的 SugarIocServices.ConfigurationSugar 回调中注册(仅主库连接 + SaaS 模式启用时):
if (iocConfig.ConfigId == App.MainDbConfigId && App.IsTenantEnabled())
{
conn.QueryFilter.AddTableFilter<SysUserMsg>(TenantFilter.SysUserMsgTenantFilter());
conn.QueryFilter.AddTableFilter<SysFile>(TenantFilter.SysFileTenantFilter());
conn.QueryFilter.AddTableFilter<SysFileGroup>(TenantFilter.SysFileGroupTenantFilter());
// 新增
conn.QueryFilter.AddTableFilter<Order>(TenantFilter.OrderTenantFilter());
}
Step 4 — 写入时填入 TenantId
SQL Sugar 的 QueryFilter 仅作用于 SELECT 查询,INSERT/UPDATE 不会经过过滤器:
public long InsertOrder(Order order)
{
order.TenantId = App.GetCurrentTenantId(); // ← 必填
return Insert(order);
}
四、角色数据权限(DataScopeExtensions + ApplyScope)
适用场景:表存租户库,不同角色按 DataScope 看不同范围的数据。
4.1 架构设计
废弃了旧的 SQL Sugar 全局 QueryFilter,改为静态注册表 + 扩展方法,由 BaseRepository 自动附加。
┌─────────────────────────┐ 启动注册 ┌──────────────────────────┐
│ SqlsugarSetup.cs │ ────────────── │ DataScopeExtensions │
│ RegisterScopeFilter() │ │ _scopeFilters 字典 │
└─────────────────────────┘ │ TryApplyScope<T>() │
└──────────┬───────────────┘
│ 查表
┌──────────▼───────────────┐
│ BaseRepository<T> │
│ Queryable() │
│ → ApplyScopeIfNeeded() │
└──────────────────────────┘
变更对照:
| 方面 | 旧方案(全局 QueryFilter) | 新方案(注册表 + ApplyScope) |
|---|---|---|
| 注册 | SqlsugarSetup 每个连接注册 AddTableFilter | SqlsugarSetup 注册到静态字典(一次) |
| 过滤时机 | 所有查询自动过滤(含内部计算逻辑) | 仅 BaseRepository.Queryable() 的查询过滤 |
| 绕过 | 需要 .ClearFilter() | 直接用 Context.Queryable<T>() 不经过 BaseRepository |
| 循环依赖风险 | 存在(内部计算被过滤导致结果为空) | 不存在(内部计算用 Context.Queryable 绕过) |
| 跨模块扩展 | 需修改 BaseRepository 源码硬编码 | 各模块调用 RegisterScopeFilter 自主注册 |
| 多租户兼容 | 需要理解连接切换与过滤器关系 | 天然兼容(权限来自 JWT,与连接无关) |
旧的
DataPermi.FilterData()已标记[Obsolete],新代码不要使用。
4.2 新增实体接入(本模块)
以在 ZR.ServiceCore 中为 SysNotice 添加数据权限为例:
Step 1 — 实体定义
[SugarTable("sys_notice")]
[Tenant("0")]
public class SysNotice
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public long NoticeId { get; set; }
public string CreateBy { get; set; } // 创建人(按用户名过滤)
public long UserId { get; set; } // 创建人ID(仅本人过滤)
public long DeptId { get; set; } // 所属部门(部门过滤)
// ... 其他字段
}
Step 2 — 编写 ApplyScope 扩展方法
在 ZR.Repository/DataScopeExtensions.cs 中添加:
public static ISugarQueryable<SysNotice> ApplyScope(this ISugarQueryable<SysNotice> query)
{
var userId = DataScopeExtensions.GetCurrentUserId();
if (userId <= 0 || DataScopeExtensions.IsAllScope()) return query;
var deptIds = DataScopeExtensions.GetDataScopeDeptIds();
var validDeptIds = deptIds.Where(d => d != 0).ToList();
if (validDeptIds.Count > 0)
return query.Where(it => it.UserId == userId || validDeptIds.Contains(it.DeptId));
return query.Where(it => it.UserId == userId);
}
Step 3 — 注册
在 ZR.ServiceCore/SqlSugar/SqlsugarSetup.cs 启动处注册:
DataScopeExtensions.RegisterScopeFilter<SysNotice>(q => q.ApplyScope());
4.3 新增实体接入(跨模块)
以 ZR.Mall 给 Product 添加权限为例。无需修改 ZR.Repository 或 ZR.ServiceCore:
Step 1 — 在模块内定义 ApplyScope(独立文件):
// ZR.Mall/ScopeExtensions.cs
public static class MallScopeExtensions
{
public static ISugarQueryable<Product> ApplyScope(this ISugarQueryable<Product> query)
{
var userId = DataScopeExtensions.GetCurrentUserId();
if (userId <= 0 || DataScopeExtensions.IsAllScope()) return query;
return query.Where(it => it.CreateUserId == userId);
}
}
Step 2 — 在模块启动处注册:
DataScopeExtensions.RegisterScopeFilter<Product>(q => q.ApplyScope());
完成后 ProductService 的 Queryable()、GetPages() 等自动附加过滤,无需在每个 Service 方法中手写。
4.4 其他常见过滤模式
仅按创建人过滤(日志类表):
public static ISugarQueryable<OrderLog> ApplyScope(this ISugarQueryable<OrderLog> query)
{
var userId = DataScopeExtensions.GetCurrentUserId();
if (userId <= 0 || DataScopeExtensions.IsAllScope()) return query;
return query.Where(it => it.CreateBy == DataScopeExtensions.GetCurrentUserName());
}
仅按用户名过滤:
public static ISugarQueryable<OrderLog> ApplyScope(this ISugarQueryable<OrderLog> query)
{
var userId = DataScopeExtensions.GetCurrentUserId();
if (userId <= 0 || DataScopeExtensions.IsAllScope()) return query;
return query.Where(it => it.UserName == DataScopeExtensions.GetCurrentUserName());
}
4.5 可用的静态方法
DataScopeExtensions 提供以下公共方法,编写 ApplyScope 时直接使用:
| 方法 | 返回值 | 说明 |
|---|---|---|
GetCurrentUserId() | long | 当前登录用户 ID(非 HTTP 返回 0) |
GetCurrentUserName() | string | 当前登录用户名 |
GetCurrentUserDeptId() | long | 当前登录用户所属部门 ID |
IsAllScope() | bool | 是否管理员 / DataScope=All |
GetDataScopeDeptIds() | IReadOnlyList<long> | 用户有权访问的部门 ID 列表(登录时缓存,120 分钟) |
GetCurrentUserRoleIds() | List<long> | 用户拥有的角色 ID 列表 |
五、使用场景
5.1 场景一:BaseService 自动过滤(推荐)
继承 BaseRepository<T> 的 Service,Queryable() 自动附加 ApplyScope,无需任何额外代码:
// BaseService<SysUser> 继承 BaseRepository<SysUser>
public PagedInfo<SysUser> GetList(SysUserQueryDto query)
{
return Queryable() // ← 自动附加 ApplyScope
.WhereIF(!string.IsNullOrEmpty(query.UserName), it => it.UserName.Contains(query.UserName))
.ToPage(query);
}
// GetId、GetFirst、Count、GetList、Any、GetPages、GetAll 均通过 Queryable() 间接过滤
var user = _sysUserService.GetId(userId);
var count = _sysUserService.Count(x => x.Status == 0);
var list = _sysUserService.GetList(x => x.Status == 0);
5.2 场景二:直接使用 db.Queryable 需手动 ApplyScope
绕过 BaseRepository 直接使用 ISqlSugarClient 时,必须手动调用 .ApplyScope():
// SysLoginService 中直接使用 db
public PagedInfo<SysLogininfor> GetLoginLog(LoginLogQueryDto query)
{
var q = db.Queryable<SysLogininfor>()
.ApplyScope() // ← 必须手动附加
.Where(exp.ToExpression())
.OrderBy(it => it.InfoId, OrderByType.Desc);
return q.ToPage(query);
}
5.3 场景三:绕过数据权限
直接使用 Context.Queryable<T>()(不经过 BaseRepository、不调用 ApplyScope):
// 计算子部门树——必须查全部部门,不能过滤
public List<long> GetChildDeptIds(long deptId)
{
var allDepts = Context.Queryable<SysDept>() // 不触发 ApplyScope
.Where(d => d.DelFlag == 0).ToList();
// ... 计算子部门 ID
}
// 查询所有用户用于下拉框——不限制权限
public List<SysUser> GetAllUsersForSelect()
{
return Context.Queryable<SysUser>() // 不触发 ApplyScope
.Where(it => it.DelFlag == 0)
.Select(it => new SysUser { UserId = it.UserId, NickName = it.NickName })
.ToList();
}
六、开发检查清单
新增接口时,按以下规则判断是否需要 .ApplyScope():
自动处理(无需手动操作)
- 通过
BaseRepository.Queryable()发起的查询 - 通过
GetId()、GetFirst()、Count()、GetList()、Any()、GetPages()、GetAll()发起的查询
需要手动添加 .ApplyScope()
- 直接调用
Context.Queryable<T>()或db.Queryable<T>() ISqlSugarClient的SqlQueryable链式调用
需要绕过权限
- 直接调用
Context.Queryable<T>()(不要调用ApplyScope()) - 内部计算逻辑(部门树构建、缓存数据加载等)
七、关键约束
- WRITE 不过滤:INSERT / UPDATE 不经过
QueryFilter和ApplyScope,写入时必须手动填TenantId/UserId/DeptId - 非 HTTP 短路:
GetCurrentUserId() <= 0时(种子数据、后台任务),各ApplyScope直接返回原query不过滤 - SELF 用户:
GetDataScopeDeptIds()仅含自己的部门 ID,SysUser.ApplyScope额外用UserId == currentUserId精确限制 - DeptId 非空:非管理员用户
DataScopeDeptIds的最小值为[自己的deptId],不会为空 - 排除 DeptId=0:
SysUser/SysDept的ApplyScope过滤掉DeptId == 0,防止泄露所有未分配部门的用户 - 部门缓存:
GetDataScopeDeptIds()读取CacheService缓存(默认 120 分钟,按租户 ID 隔离),登录时预计算,非每次查询实时计算
八、关键类说明
| 类 | 位置 | 说明 |
|---|---|---|
DataPermiEnum | ZR.ServiceCore/SqlSugar/DataPermi.cs | 角色配置级数据权限枚举(All/CUSTOM/DEPT/DEPT_CHILD/SELF) |
MergedScopeType | Infrastructure/Model/LoginUser.cs | 登录时合并后的权限等级(None/Self/Dept/DeptList/All) |
DataScopeExtensions | ZR.Repository/DataScopeExtensions.cs | 静态注册表 + 5 个内置 ApplyScope + 用户/部门信息读取 |
BaseRepository | ZR.Repository/BaseRepository.cs | 通用仓储,Queryable() 通过 TryApplyScope 自动附加已注册的过滤 |
BaseService | 继承 BaseRepository<T> | 所有 Service 通过 Queryable() 间接获得权限过滤 |
SqlsugarSetup | ZR.ServiceCore/SqlSugar/SqlsugarSetup.cs | 启动时注册 TenantFilter(AddTableFilter)和 DataScopeExtensions(RegisterScopeFilter) |
TenantFilter | ZR.ServiceCore/SqlSugar/TenantFilter.cs | 租户隔离过滤表达式(主库共享实体按 TenantId 过滤) |
IMainDbEntity | ZR.Model/MainDbAttribute.cs | 标记接口,实现该接口的实体路由到主库 |
