你以为加个 TenantId 字段就算隔离了,其实还差三道防线

大家好,我是码农刚子。这是「秦巴牧云踩坑录」第 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。手写十次,漏一次就是一桩事故。

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

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

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

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

三、四道隔离防线

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

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

csharp 复制代码
modelBuilder.Entity<Feeds>()
    .HasQueryFilter(f => f.TenantId == _currentTenantId);
modelBuilder.Entity<BreedingRecords>()
    .HasQueryFilter(b => b.TenantId == _currentTenantId);

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

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

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

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

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

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

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

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

结尾

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

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

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

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


关于作者

码农刚子,六年 ERP / 制造业系统开发,专注 C# / .NET / Blazor。 写 PCB 行业 ERP 与多租户 SaaS 的实战与踩坑,不写面试八股。

博客:https://www.coderlog.net/ | 公众号:CSharp精选营

相关推荐
gwf2166 天前
AI多租户集群的RDMA隔离:PD/MR/ACL硬件实现 —— 面向大模型训练/推理的硅级安全架构
芯片设计·rdma·nccl·ai集群·gpudirect·多租户隔离·sr-iov
夏天一说说2 个月前
深度解析 SaaS 平台权限鉴权漏洞:仅修改请求参数即可解锁企业版核心功能
burpsuite·越权漏洞·saas安全·权限漏洞·src漏洞
鱼听禅2 个月前
C#学习笔记-Entity Framework Core基础操作学习
c#·orm·ef core
CSharp精选营4 个月前
AI 开发狂飙!.NET 11 Preview 4 原生集成向量搜索 + MCP 模板,EF Core 直接对标 RAG 应用
rag·向量搜索·ef core·mcp·.net 11
CSharp精选营4 个月前
.NET 8 Web开发入门(四):注入燃料——Entity Framework Core 与 Code First 实战
orm·sql server·数据库迁移·ef core·entity framework core·crud操作·code first
硅基喵5 个月前
开发实战:asp.net core + ef core 实现动态可扩展的分页方案
ef core·工程实践
小邓的技术笔记6 个月前
EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界
ef core
硅基喵6 个月前
EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界
ef core·工程实践
硅基喵6 个月前
EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地
架构设计·ef core