ASP.NET Core 8:超级用户全局忽略HasQueryFilter及代码问题排查
全局让SuperUser绕过EF Core租户查询过滤器的解决方案
问题背景
采用单数据库单代码库的多租户架构,通过EF Core的查询过滤器隔离各租户数据,但需要让SuperUser角色的用户全局绕过该过滤器,无需在每个查询中手动添加IgnoreQueryFilters()。此前尝试在OnModelCreating中通过判断用户角色来条件添加过滤器,但未生效。
原代码失效原因
OnModelCreating是应用启动时的模型构建阶段执行的方法,此时没有请求上下文,且模型会被全局缓存,不会随每个请求重新构建。因此这里的用户角色判断只会执行一次(应用启动时),无法根据当前请求的用户动态调整过滤器逻辑。
正确实现思路
不要在模型构建阶段条件添加过滤器,而是在过滤器表达式中直接嵌入用户角色判断,让EF Core每次执行查询时都动态计算该条件。同时需要注意:
- 避免在过滤器中执行额外数据库查询(会引发EF Core异常),应将租户ID存储在用户Claims中,从HttpContext直接读取。
- 确保注册
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.
相关产品推荐
相关产品推荐

