这是「秦巴牧云踩坑录」第 2 篇,复盘真实项目里那些差点出事的坑。

说真的,TenantId 这东西被不少人神话了。以为给每张表塞个字段,多租户隔离就齐活了。我们在秦巴牧云栽过跟头,三个盲区,一个比一个隐蔽。

秦巴牧云是我用 .NET 8 写的畜牧 SaaS。Api、Web、小程序三端共用一个后端。

所有租户的数据进同一个库,靠 Tenants 表分开。画架构图的时候,隔离那道线画得漂漂亮亮。

等代码真跑起来,A 租户的数据就溜进了 B 租户的报表。我盯着那条错的数据看了好几秒,后背有点凉。多租户这事儿,画个框远远不够。

一、TenantId 只是个标记

TenantId 说白了就是个标记字段。它记着这行数据是谁的,但拦不住任何一次查询。

我们最早的 BreedingRecords 查询,就是乖乖按 TenantId 过滤。可一旦联表忘了写 Where,Feeds 表的数据当场就串出去了。一个字段,保不住一整张表。

最阴的是聚合报表。SUM、GROUP BY 一漏 TenantId,两个养殖场的饲料用量就混在一张表里。屏幕上是数字,背后是两家场子在共享账本。

分页也踩过。Skip、Take 前面的 Where 一漏,A 租户翻到第 3 页,看到的全是 B 租户的养殖记录。

还有导航属性。Include 进来的子表要是不带过滤,A 租户查自己的牛群,顺手把 B 租户的免疫记录也拽出来了。

根子都在查询层。每一行取数据的代码都得自己记得带租户条件,人哪能次次都记得。

二、我们踩过的三个盲区

第一个盲区,DbContext 工厂没按租户绑定。我用 IDbContextFactory 建上下文,它却老老实实返回同一个默认连接。

切换租户的时候,上下文还揣着上一个租户的数据。这问题平时不显,一到高峰期频繁切租户才露馅。

生产告警第一次响,是因为有个养殖场老板看到了隔壁场的存栏数。那天我才反应过来,工厂模式我压根没用对地方。

第二个盲区,缺了全局查询过滤器。EF Core 的 HasQueryFilter 本来能每次查询自动补上 TenantId。

上线前我们没配。所有 LINQ 全靠手写 Where。手写十次,漏一次就是一桩事故。

// 漏掉 HasQueryFilter 时,每个查询都要人工保证带条件
var list = await db.Feeds
    .Where(f => f.TenantId == tid)
    .ToListAsync();

第三个盲区最要命,租户解析不靠谱。中间件从 JWT 里拿 TenantId,小程序那边有时候传个空值上来。

测试环境就一个租户,这个坑从来没人踩过。上线后真有流量了,空值第一次从校验缝里溜了过去。

空值意味着中间件退回到默认租户,而默认租户正好是我们的测试租户,真实数据就这么被错标了。三个盲区摞一块儿,隔离墙看着砌结实了,门压根没上锁。

三、四道隔离防线

第一道,用全局查询过滤器兜底。在 OnModelCreating 里给每个实体配 HasQueryFilter,从此任何 LINQ 查询都自带条件。

手写 Where 从必需变成了多余。哪怕谁手滑漏写,过滤器也兜得住。

modelBuilder.Entity<Feeds>()
    .HasQueryFilter(f => f.TenantId == _currentTenantId);
modelBuilder.Entity<BreedingRecords>()
    .HasQueryFilter(b => b.TenantId == _currentTenantId);

第二道,DbContext 工厂按租户绑定。建上下文的时候,从租户解析器把当前 TenantId 注进去,再强制开个新连接。

上下文不再揣着上一个租户的状态。切租户,等于拿到一个干干净净的新上下文。

第三道,租户解析中间件加一道鉴权校验。JWT 里的 TenantId 得跟当前登录用户所属租户对上。

对不上,直接甩 403。空值也别想再悄悄溜过去。

// 租户解析中间件双校验
if (jwtTenantId != user.TenantId)
{
    context.Response.StatusCode = 403;
    return;
}

第四道,自动化租户隔离测试。我写了条断言,拿租户 A 的令牌去查,结果里一个 TenantId 不是 A 的行都不许出现。

这条测试进 CI,每次提交都跑。以前靠人盯的隔离检查,现在交给机器每次必查。

四道防线落下去之后,秦巴牧云再没闹过租户串数据。那次最贵的教训,把隔离从架构图,一寸寸挪进了每一行查询里。

结尾

多租户隔离是持续活儿,不是配一次就完事。TenantId 是门票,四道防线才是护城河。

你在多租户上栽过最贵的坑是啥?评论区唠唠。

转发给正要上 SaaS 的朋友,少走我们趟过的弯路。

关注 CSharp精选营,每周二四 get 能直接抄的 C# 实战。