You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 8:超级用户全局忽略HasQueryFilter及代码问题排查

全局让SuperUser绕过EF Core租户查询过滤器的解决方案

问题背景

采用单数据库单代码库的多租户架构,通过EF Core的查询过滤器隔离各租户数据,但需要让SuperUser角色的用户全局绕过该过滤器,无需在每个查询中手动添加IgnoreQueryFilters()。此前尝试在OnModelCreating中通过判断用户角色来条件添加过滤器,但未生效。

原代码失效原因

OnModelCreating是应用启动时的模型构建阶段执行的方法,此时没有请求上下文,且模型会被全局缓存,不会随每个请求重新构建。因此这里的用户角色判断只会执行一次(应用启动时),无法根据当前请求的用户动态调整过滤器逻辑。

正确实现思路

不要在模型构建阶段条件添加过滤器,而是在过滤器表达式中直接嵌入用户角色判断,让EF Core每次执行查询时都动态计算该条件。同时需要注意:

  1. 避免在过滤器中执行额外数据库查询(会引发EF Core异常),应将租户ID存储在用户Claims中,从HttpContext直接读取。
  2. 确保注册IHttpContextAccessor,用于获取当前请求上下文。

修改后的完整代码

1. ApplicationDbContext 调整

using Whatever.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;

namespace Whatever.Data
{
    public partial class ApplicationDbContext : 
        IdentityDbContext<ApplicationUser, ApplicationRole, string,
        IdentityUserClaim<string>, ApplicationUserRole, 
        IdentityUserLogin<string>, IdentityRoleClaim<string>, 
        IdentityUserToken<string>>
    {
        private readonly IHttpContextAccessor _contextAccessor;

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor httpContextAccessor)
            : base(options)
        {
            _contextAccessor = httpContextAccessor;
        }

        public virtual DbSet<ApplicationUser> ApplicationUser { get; set; }
        public virtual DbSet<ApplicationRole> ApplicationRole { get; set; }
        public virtual DbSet<ApplicationUserRole> ApplicationUserRole { get; set; }
        public virtual DbSet<Tenant> Tenant { get; set; }
        public virtual DbSet<TenantPerson> TenantPerson { get; set; }
        public virtual DbSet<Customer> Customer { get; set; }
        public virtual DbSet<CustomerMember> CustomerMember { get; set; }              
        public virtual DbSet<Project> Project { get; set; }
        public virtual DbSet<Profile> Profile { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);           

            modelBuilder.Entity<ApplicationUserRole>(userRole =>
            {
                userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

                userRole.HasOne(ur => ur.Role)
                    .WithMany(r => r.UserRoles)
                    .HasForeignKey(ur => ur.RoleId)
                    .IsRequired();

                userRole.HasOne(ur => ur.User)
                    .WithMany(r => r.UserRoles)
                    .HasForeignKey(ur => ur.UserId)
                    .IsRequired();
            });      

            // 给租户相关实体添加带SuperUser判断的查询过滤器
            modelBuilder.Entity<Tenant>().HasQueryFilter(e => 
                e.Id == CurrentTenantId || IsSuperUser);
            modelBuilder.Entity<TenantPerson>().HasQueryFilter(e => 
                e.TenantId == CurrentTenantId || IsSuperUser);
            modelBuilder.Entity<Customer>().HasQueryFilter(e => 
                e.TenantId == CurrentTenantId || IsSuperUser);
            modelBuilder.Entity<CustomerMember>().HasQueryFilter(e => 
                e.TenantId == CurrentTenantId || IsSuperUser);

            modelBuilder.Entity<Project>()
                .HasOne(e => e.Profile)
                .WithOne(e => e.Project)
                .HasForeignKey<Profile>(e => e.ProjectId)
                .IsRequired(false);
        }

        // 判断当前用户是否为SuperUser
        private bool IsSuperUser => 
            _contextAccessor.HttpContext?.User.IsInRole("SuperUser") == true;

        // 从用户Claims中获取当前租户ID(需在登录时注入该Claim)
        public virtual Guid CurrentTenantId
        {
            get
            {
                if (_contextAccessor.HttpContext == null)
                    return Guid.Empty;

                var tenantIdClaim = _contextAccessor.HttpContext.User.FindFirst("TenantId");
                if (Guid.TryParse(tenantIdClaim?.Value, out var tenantId))
                    return tenantId;

                return Guid.Empty;
            }
        }
    }
}

2. Program.cs 调整(添加IHttpContextAccessor注册)

using Whatever.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// 注册IHttpContextAccessor,用于获取请求上下文
builder.Services.AddHttpContextAccessor();

var connectionString = 
    builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddIdentity<ApplicationUser, ApplicationRole> 
    (options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

// 注册自定义Claims工厂,登录时将TenantId注入用户Claims
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");        
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Customer}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

3. 自定义Claims工厂(登录时注入TenantId)

using Whatever.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;

namespace Whatever.Models
{
    public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
    {
        public CustomUserClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, 
            RoleManager<ApplicationRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
        {
            var identity = await base.GenerateClaimsAsync(user);
            // 将用户的TenantId添加到Claims集合
            if (user.TenantId != Guid.Empty)
            {
                identity.AddClaim(new Claim("TenantId", user.TenantId.ToString()));
            }
            return identity;
        }
    }
}

关键说明

  • 过滤器动态计算:每个租户实体的过滤器包含两个条件,当用户是SuperUser时,第二个条件为真,自动绕过租户隔离。
  • 避免循环查询:通过Claims获取租户ID,而非在过滤器中查询数据库,避免EF Core执行查询时引发异常。
  • 上下文注册:必须注册IHttpContextAccessor,否则无法获取当前请求的用户信息。

内容的提问来源于stack exchange,提问作者Charles de M.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 15:47:07