MoneyTree.DataPermission 1.0.3
MoneyTree.DataPermission 数据权限
概述
MoneyTree.DataPermission 提供框架级数据权限基础设施——AsyncLocal 权限范围传播、中间件管道、DI 注册入口和 EF Core HasQueryFilter 注册入口。
框架层不预设任何业务逻辑(如部门、区域、仅本人数据等),由业务系统根据实际需求实现权限范围解析和查询过滤表达式。
| 属性 | 说明 |
|---|---|
| NuGet 包 | MoneyTree.DataPermission |
| 外部依赖 | MoneyTree.Core、ASP.NET Core、EF Core |
| 定位 | 框架级数据权限基础设施(业务逻辑由业务系统实现) |
设计原则
框架只提供管道,不提供业务。
| 层 | 职责 | 提供方 |
|---|---|---|
| 框架 | AsyncLocal 权限范围传播 | IDataPermissionAccessor<T> |
| 框架 | 中间件管道 | DataPermissionMiddleware |
| 框架 | DI 注册入口 | AddMoneyTreeDataPermission() |
| 框架 | HasQueryFilter 注册入口 | ConfigureDataPermission<T>() |
| 框架 | 权限范围基类 | DataPermissionScope(仅 IsAdmin) |
| 业务 | 权限范围类型 | 继承 DataPermissionScope 扩展自定义维度 |
| 业务 | 权限范围解析 | 实现 IDataPermissionProvider<T> |
| 业务 | 查询过滤表达式 | 提供 Expression<Func<TEntity, bool>> |
这与 MoneyTree.MultiTenancy 的模式一致:框架提供 IMultiTenant + ConfigureTenantFilters(Func<long?>),业务系统提供租户ID访问委托。
项目文件结构
MoneyTree.DataPermission/
├── MoneyTree.DataPermission.csproj
├── GlobalUsings.cs
├── Abstractions/
│ ├── IDataPermissionAccessor.cs # AsyncLocal 权限范围传播(非泛型接口 + 泛型具体类)
│ ├── IDataPermissionProvider.cs # 权限范围解析抽象 + DataPermissionScope 基类 + 适配器
│ └── DataPermissionOptions.cs # 配置选项(失败模式 FailOpen/FailClosed)
├── Attributes/
│ └── DataPermissionAttribute.cs # 空标记特性(业务可继承扩展)
├── Extensions/
│ └── DataPermissionExtensions.cs # DI 注册 + ConfigureDataPermission 入口
└── Middleware/
└── DataPermissionMiddleware.cs # 中间件(调用 Provider 解析并设置 AsyncLocal,支持失败模式)
核心类型
DataPermissionScope — 权限范围基类
public abstract class DataPermissionScope
{
/// <summary>
/// 是否为管理员(无任何数据限制)。
/// 业务系统在解析权限时显式设置,框架不自动推断。
/// </summary>
public virtual bool IsAdmin { get; set; }
}
业务系统继承此类扩展自定义权限维度(如部门ID、区域代码等)。
IDataPermissionProvider — 权限范围解析接口
public interface IDataPermissionProvider<T> where T : DataPermissionScope
{
Task<T?> GetPermissionScopeAsync();
}
业务系统实现此接口,从 HttpContext/数据库/缓存中解析当前用户的权限范围。
IDataPermissionAccessor / DataPermissionAccessor — AsyncLocal 权限范围传播
框架提供非泛型接口 IDataPermissionAccessor(中间件注入)和泛型具体类 DataPermissionAccessor<T>(DbContext 注入):
/// 非泛型接口:中间件通过此接口设置权限范围
public interface IDataPermissionAccessor
{
DataPermissionScope? Scope { get; set; }
}
/// 泛型具体类:DbContext 通过此类型读取泛型权限范围
public class DataPermissionAccessor<T> : IDataPermissionAccessor where T : DataPermissionScope
{
// 泛型访问(as T 转换),与接口实现共享同一 static AsyncLocal 存储槽位
public new T? Scope { get; set; }
}
基于 static AsyncLocal<DataPermissionScope?> 实现跨异步上下文的权限范围传播。中间件通过非泛型接口写入基类引用,DbContext 通过泛型具体类读取子类引用,两者共享同一存储槽位。
DataPermissionOptions — 失败模式配置
/// 数据权限解析失败时的处理模式
public enum DataPermissionFailureMode
{
FailOpen, // 容错优先(默认):解析异常时不设置 scope,继续执行
FailClosed // 安全优先:解析异常或已认证用户未注册 Provider 时返回 403
}
/// 数据权限配置选项
public class DataPermissionOptions
{
public DataPermissionFailureMode FailureMode { get; set; } = DataPermissionFailureMode.FailOpen;
}
请求处理流程
HTTP 请求 → 认证 → 授权 → [DataPermissionMiddleware] → 控制器 → EF Core 查询
│
▼
IDataPermissionProvider.GetPermissionScopeAsync()
(业务系统实现,从 HttpContext/DB 解析权限范围)
│
▼
IDataPermissionAccessor.Scope = scope
(设置到 AsyncLocal,供 HasQueryFilter 读取)
│
▼
请求结束后 finally 清理 AsyncLocal
快速开始
1. 定义业务权限范围
using MoneyTree.DataPermission.Abstractions;
/// <summary>
/// 业务自定义权限范围(继承 DataPermissionScope 扩展维度)。
/// </summary>
public class MyPermissionScope : DataPermissionScope
{
/// <summary>当前用户所属部门ID</summary>
public long? DepartmentId { get; set; }
/// <summary>允许访问的部门ID列表</summary>
public List<long> AllowedDepartmentIds { get; set; } = [];
/// <summary>当前用户ID</summary>
public string? CurrentUserId { get; set; }
}
2. 实现权限范围解析
using MoneyTree.DataPermission.Abstractions;
/// <summary>
/// 从 JWT Claims 解析权限范围。
/// </summary>
public class MyPermissionProvider : IDataPermissionProvider<MyPermissionScope>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyPermissionProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task<MyPermissionScope?> GetPermissionScopeAsync()
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
return Task.FromResult<MyPermissionScope?>(null);
var scope = new MyPermissionScope
{
CurrentUserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
DepartmentId = long.TryParse(user.FindFirst("department_id")?.Value, out var deptId) ? deptId : null,
IsAdmin = user.IsInRole("Admin")
};
// 从 Claims 提取允许访问的部门列表
scope.AllowedDepartmentIds = user.FindAll("allowed_dept")
.Select(c => long.Parse(c.Value))
.ToList();
return Task.FromResult<MyPermissionScope?>(scope);
}
}
3. 注册服务
// Program.cs
using MoneyTree.DataPermission.Extensions;
var builder = WebApplication.CreateBuilder(args);
// 注册数据权限基础设施 + 自定义权限范围类型
// 安全敏感场景可设置 FailClosed 模式:权限解析失败时返回 403 拒绝访问
builder.Services.AddMoneyTreeDataPermission<MyPermissionScope>(options =>
{
options.FailureMode = DataPermissionFailureMode.FailClosed; // 默认 FailOpen
});
// 注册业务权限范围解析(Scoped)
builder.Services.AddScoped<IDataPermissionProvider<MyPermissionScope>, MyPermissionProvider>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseMoneyTreeDataPermission(); // 必须在认证授权之后
app.Run();
4. DbContext 中配置查询过滤
using MoneyTree.DataPermission.Extensions;
using MoneyTree.DataPermission.Abstractions;
public class AppDbContext : MoneyTreeDbContext
{
// 直接注入具体类型 DataPermissionAccessor<T>,访问泛型 Scope 属性
private readonly DataPermissionAccessor<MyPermissionScope> _accessor;
public AppDbContext(
DbContextOptions<AppDbContext> options,
DataPermissionAccessor<MyPermissionScope> accessor) : base(options)
{
_accessor = accessor;
}
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 注册数据权限查询过滤——业务系统提供完整表达式
modelBuilder.ConfigureDataPermission<Order>(e =>
_accessor.Scope == null // 权限未解析(后台服务等):无限制
|| _accessor.Scope.IsAdmin // 管理员:无限制
|| _accessor.Scope.AllowedDepartmentIds.Contains(
EF.Property<long>(e, "DepartmentId"))); // 按部门过滤
}
}
与 MultiTenancy 的关系
| 维度 | MultiTenancy | DataPermission |
|---|---|---|
| 筛选维度 | TenantId = @CurrentTenantId |
业务系统自定义(如部门、区域等) |
| 粒度 | 租户级 | 租户内部的组织/用户级 |
| 实现 | ConfigureTenantFilters(Func<long?>) |
ConfigureDataPermission(Expression) |
| 权限范围 | 固定 long? 租户ID |
业务系统自定义 DataPermissionScope 子类 |
| 关系 | 互补,同一请求中两者同时生效 | — |
两者通过 HasQueryFilter 链式叠加,EF Core 会自动合并多个查询过滤器为 WHERE tenant_filter AND data_permission_filter。
注意事项
- 业务系统必须注册
IDataPermissionProvider<T>:框架的中间件通过此接口解析权限范围,未注册则跳过权限解析(Scope为 null)。 Scope == null的语义:表示权限未解析(如后台服务、未认证请求、Provider 解析失败),由业务系统的HasQueryFilter表达式决定是否限制(通常为无限制)。- Provider 解析失败的容错行为:由
DataPermissionOptions.FailureMode控制,默认FailOpen:FailOpen(默认,向后兼容):中间件 catch 异常后记录LogError,不返回 403,请求继续执行(Scope为 null → 由 HasQueryFilter 表达式决定,通常为无限制)。适用于可用性优先的场景。FailClosed(安全优先):中间件 catch 异常后返回 403 拒绝访问;已认证用户未注册 Provider 时也返回 403。与MultiTenancyMiddleware容错策略一致,适用于金融、医疗等安全敏感场景。- 注意:Provider 正常返回
null(用户无特殊权限维度)在两种模式下都不被拒绝,由 HasQueryFilter 表达式决定。
- AsyncLocal 清理:中间件在
finally中清理 AsyncLocal,防止跨请求泄漏。 - 表达式树限制:
HasQueryFilter接收Expression<Func<T, bool>>,表达式必须可被 EF Core 翻译为 SQL,不能使用语句体 lambda 或 if/else 分支。 - 与 MultiTenancy 叠加:两者通过
HasQueryFilter链式叠加,EF Core 自动合并为WHERE tenant_filter AND data_permission_filter。
No packages depend on MoneyTree.DataPermission.
.NET 10.0
- MoneyTree.Core (>= 1.0.3)
- Microsoft.EntityFrameworkCore (>= 10.0.9)