租户隔离需全链路自动识别:在请求入口解析租户标识存入AsyncLocal或HttpContext.Items,数据访问层自动读取;EF Core用HasQueryFilter全局过滤,租户配置存Tenant表并内存缓存,日志与异常须携带TenantId。

如何识别并隔离租户数据
多租户不是加个字段就完事,核心是让同一套代码在运行时自动区分租户边界。最常见错误是把 TenantId 当成普通参数手动传——漏传、错传、缓存污染全由此来。
推荐做法是:在请求入口(如 ASP.NET Core 的 Middleware)中解析租户标识(域名、Header、子路径),存入 AsyncLocal 或 HttpContext.Items,后续所有数据访问层自动读取,不依赖业务代码显式传递。
- 别用静态变量存
TenantId—— 多线程下会串租户 - 数据库连接字符串不能按租户切换(除非物理隔离),优先走逻辑隔离 + 全局过滤(如 EF Core 的
HasQueryFilter) - 若用共享数据库,每个租户表必须有
TenantId字段,且所有查询默认带.Where(x => x.TenantId == current)
EF Core 怎么自动注入租户过滤
手动写 Where 容易遗漏,EF Core 提供了 HasQueryFilter 做全局拦截,但要注意它只对实体生效,不作用于原始 SQL 或 FromSqlRaw。
示例:在 OnModelCreating 中为所有租户敏感实体统一加过滤:
modelBuilder.Entity<order>().HasQueryFilter(x => x.TenantId == _tenantService.GetCurrentTenantId());</order>
-
_tenantService必须是 Scoped 生命周期,不能是 Singleton,否则跨请求取到旧租户 - 软删除字段(如
IsDeleted)和TenantId过滤可组合使用,但注意HasQueryFilter不支持异步委托,GetCurrentTenantId()得是同步方法 - 执行
IgnoreQueryFilters()会绕过租户检查——仅限后台管理或迁移脚本,切勿在用户请求中开放
租户配置怎么避免硬编码
租户的连接字符串、功能开关、主题色这些信息如果写死在 appsettings.json 里,每次新增租户都要改配置、重启服务。实际项目中应把租户元数据存进独立的 Tenant 表,启动时加载进内存缓存(如 IMemoryCache)。
- 缓存键建议用
"tenant:" + domain,避免用自增 ID——域名才是运行时唯一入口 - 不要在
Startup.ConfigureServices里直接查数据库加载租户配置,EF Context 尚未构建完成;改用IHostedService或懒加载 - 配置变更后,需主动清除对应租户缓存,不能等自然过期——否则新配置几小时都不生效
为什么租户间日志和异常不能混在一起
一个租户报错,日志里看不到 TenantId,排查时得翻 N 个请求头拼上下文,这是典型设计缺失。ASP.NET Core 的 ILogger 支持结构化日志,但默认不携带租户上下文。
- 在中间件里调用
logger.BeginScope(new Dictionary{ ["TenantId"] = tenantId }) - 全局异常过滤器(
ExceptionFilterAttribute)中,必须从HttpContext取当前租户再记录,而不是从异常对象里猜 - Serilog 用户可配
Enrich.WithProperty("TenantId", ...),但注意该属性值必须是线程安全获取的,别用闭包捕获异步上下文
租户隔离真正难的不是代码怎么写,而是所有环节——从 HTTP 请求进来,到 DB 查询、缓存读写、消息队列投递、甚至定时任务触发——都得能稳定拿到且信任那个 TenantId。少一个环节,就埋一个越权或数据错乱的坑。











