MoneyTree.DataPermission 1.0.4

MoneyTree.DataPermission 数据权限

概述

MoneyTree.DataPermission 提供框架级数据权限基础设施——AsyncLocal 权限范围传播、中间件管道、DI 注册入口和 EF Core HasQueryFilter 注册入口。

框架层不预设任何业务逻辑(如部门、区域、仅本人数据等),由业务系统根据实际需求实现权限范围解析和查询过滤表达式。

属性 说明
NuGet 包 MoneyTree.DataPermission
外部依赖 MoneyTree.CoreASP.NET CoreEF 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


注意事项

  1. 业务系统必须注册 IDataPermissionProvider<T>:框架的中间件通过此接口解析权限范围,未注册则跳过权限解析(Scope 为 null)。
  2. Scope == null 的语义:表示权限未解析(如后台服务、未认证请求、Provider 解析失败),由业务系统的 HasQueryFilter 表达式决定是否限制(通常为无限制)。
  3. Provider 解析失败的容错行为:由 DataPermissionOptions.FailureMode 控制,默认 FailOpen
    • FailOpen(默认,向后兼容):中间件 catch 异常后记录 LogError,不返回 403,请求继续执行(Scope 为 null → 由 HasQueryFilter 表达式决定,通常为无限制)。适用于可用性优先的场景。
    • FailClosed(安全优先):中间件 catch 异常后返回 403 拒绝访问;已认证用户未注册 Provider 时也返回 403。与 MultiTenancyMiddleware 容错策略一致,适用于金融、医疗等安全敏感场景。
    • 注意:Provider 正常返回 null(用户无特殊权限维度)在两种模式下都不被拒绝,由 HasQueryFilter 表达式决定。
  4. AsyncLocal 清理:中间件在 finally 中清理 AsyncLocal,防止跨请求泄漏。
  5. 表达式树限制HasQueryFilter 接收 Expression<Func<T, bool>>,表达式必须可被 EF Core 翻译为 SQL,不能使用语句体 lambda 或 if/else 分支。
  6. 与 MultiTenancy 叠加:两者通过 HasQueryFilter 链式叠加,EF Core 自动合并为 WHERE tenant_filter AND data_permission_filter

No packages depend on MoneyTree.DataPermission.

.NET 10.0

Version Downloads Last updated
1.0.4 2 7/21/2026
1.0.3 1 7/12/2026