如何在EF Core的DbContext中为所有查询添加参数保障租户数据安全?
实现EF Core自动租户过滤与实体赋值的方案
嘿,这个需求我之前在多租户项目里正好落地过,结合EF Core的特性完全能优雅解决,不用在每个仓储或查询里重复写过滤逻辑,下面给你一步步拆解实现:
1. 统一租户实体规范
首先得给所有需要租户隔离的实体定个统一标准——让它们都实现ITenantEntity接口,这样后续的全局处理才有依据:
public interface ITenantEntity { Guid TenantId { get; set; } } // 示例实体,比如商品类 public class Product : ITenantEntity { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public Guid TenantId { get; set; } // 共享表的租户标识字段 }
2. 封装租户上下文服务
我们需要一个能获取当前用户所属租户ID的服务,这里假设租户ID存在用户的Claims中(你也可以根据自己的认证方式调整):
public interface ITenantContext { Guid CurrentTenantId { get; } } public class HttpTenantContext : ITenantContext { private readonly IHttpContextAccessor _httpContextAccessor; public HttpTenantContext(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Guid CurrentTenantId { get { var tenantClaim = _httpContextAccessor.HttpContext?.User?.FindFirst("TenantId"); if (tenantClaim == null || !Guid.TryParse(tenantClaim.Value, out var tenantId)) { throw new InvalidOperationException("当前用户未关联有效租户信息"); } return tenantId; } } }
3. 给DbContext添加全局查询过滤器
EF Core的HasQueryFilter能帮我们给所有查询自动加上租户过滤条件,不需要在每个LINQ查询里手动写:
public class AppDbContext : DbContext { private readonly ITenantContext _tenantContext; // 暴露当前租户ID供过滤器使用 public Guid CurrentTenantId => _tenantContext.CurrentTenantId; public AppDbContext(DbContextOptions<AppDbContext> options, ITenantContext tenantContext) : base(options) { _tenantContext = tenantContext; } // 你的DbSet集合 public DbSet<Product> Products { get; set; } // ...其他DbSet protected override void OnModelCreating(ModelBuilder modelBuilder) { // 遍历所有实现ITenantEntity的实体,自动添加租户过滤 foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType)) { // 构建Lambda表达式:entity => entity.TenantId == CurrentTenantId var param = Expression.Parameter(entityType.ClrType, "entity"); var tenantIdProp = Expression.Property(param, nameof(ITenantEntity.TenantId)); var currentTenantProp = Expression.Property(Expression.Constant(this), nameof(CurrentTenantId)); var filterExpr = Expression.Lambda(Expression.Equal(tenantIdProp, currentTenantProp), param); modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterExpr); } } // 其他模型配置(比如主键、关系)... } }
4. 重写SaveChanges自动设置TenantId
接下来要确保新增实体时自动赋值TenantId,同时防止手动设置错误的租户ID,重写DbContext的保存方法即可:
public override int SaveChanges(bool acceptAllChangesOnSuccess) { AutoSetTenantIds(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { AutoSetTenantIds(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void AutoSetTenantIds() { // 找到所有新增的ITenantEntity类型实体 var newTenantEntities = ChangeTracker.Entries<ITenantEntity>() .Where(e => e.State == EntityState.Added); foreach (var entry in newTenantEntities) { // 自动赋值TenantId(如果还没设置) if (entry.Entity.TenantId == Guid.Empty) { entry.Entity.TenantId = CurrentTenantId; } // 可选:校验防止手动设置错误租户ID else if (entry.Entity.TenantId != CurrentTenantId) { throw new InvalidOperationException("禁止为当前租户以外的实体创建数据"); } } }
5. 依赖注入配置
最后在Startup里注册相关服务,确保DbContext能拿到租户上下文:
public void ConfigureServices(IServiceCollection services) { // 注册HttpContextAccessor(需要手动添加) services.AddHttpContextAccessor(); // 注册租户上下文 services.AddScoped<ITenantContext, HttpTenantContext>(); // 注册DbContext services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // 注册你的通用仓储和工作单元... }
额外注意事项
- 绕过过滤器:如果需要超级管理员查看所有租户数据,可以在查询时调用
IgnoreQueryFilters(),比如dbContext.Products.IgnoreQueryFilters().ToList() - 实体覆盖:确保所有需要租户隔离的实体都实现
ITenantEntity,否则过滤器不会生效 - 仓储兼容性:你的通用仓储和工作单元模式完全兼容这套方案,因为所有查询和保存操作都会经过DbContext的逻辑,不需要修改仓储代码
这样配置后,你的应用就能自动处理租户隔离了,既保证了数据安全,又彻底避免了重复写TenantId过滤的逻辑~
内容的提问来源于stack exchange,提问作者Marek Urbanowicz
相关产品推荐
相关产品推荐

