码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • 你以为加个 TenantId 字段就算隔离了,其实还差三道防线


    文章指出仅靠TenantId不够,存在上下文工厂未绑定、缺全局查询过滤器、租户解析回退默认租户三盲区;提出全局过滤器、按租户建上下文、中间件鉴权、自动化测试四道防线。
    合集 - C#/.NET 编程指南(22)
    1.记一次SQL server2008 数据库事务日志已满,导致程序崩溃排查过程03-052..NET 权限系统(RBAC)怎么设计?直接可复用03-093..NET命名之谜:它与C#纠缠20年的关系揭秘03-134.VSCode周更时代:开发者必备的3个应对策略03-185.SQL Server安装避坑:这8个奇葩报错你遇到过几个?03-186..NET对象转JSON,到底有几种方式?03-247.C# 面试高频题:装箱和拆箱是如何影响性能的?03-258.值类型与引用类型:别再只背“栈和堆”了,看这 4 个实际影响03-299.聊一聊 C# 中的闭包陷阱:foreach 循环的坑你还记得吗?03-3010.都是微软亲儿子,WPF凭啥干不掉WinForm?这3个场景说明白了04-0811.C# 如何减少代码运行时间:7 个实战技巧04-1312.for和foreach到底谁快?刚子跑了1亿次循环,告诉你真相04-1513.AutoMapper三板斧:值转换器、条件映射、自定义解析器,复杂映射不再愁04-1714.C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生04-1915.序列化 JSON 时崩了?99% 是 EF 延迟加载惹的祸,三种解法拿走不谢04-2116.写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑04-2217.EF Core 8 + SQL Server:Contains() 突然报 "关键字 WITH 附近有语法错误"?一篇避坑指南06-0918..NET 8 与 .NET 9 支持终止倒计时:开发者需要了解什么07-0319.Blazor Server 中使用 MiniExcel 实现 Excel 导出与导入 — 实战教程08-0520.用 .NET 8 + uni-app 做一套多租户畜牧 SaaS:我们在秦巴牧云踩过的 6 个坑08-24
    21.你以为加个 TenantId 字段就算隔离了,其实还差三道防线09-17
    22.聊聊 Blazor 里 Radzen 6.0.0 那几个用着别扭的官方组件09-24
    收起

    大家好,我是码农刚子。这是「秦巴牧云踩坑录」第 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# 实战。


     

    关于作者

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

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

  • 相关阅读:
    【】如何实现异步通知的重试机制
    高等数学(第七版)同济大学 习题10-1 个人解答
    Linux下自动删除过期备份和自动异地备份的脚本
    android的USB开发时 mUsbManager.getDeviceList()获取都为空
    Spring的AOP介绍和使用
    python正则表达式
    基于JAVA医院患者管理系统计算机毕业设计源码+系统+数据库+lw文档+部署
    休闲娱乐 - 挂耳咖啡
    十六、保存和加载自己所搭建的网络模型
    react中数组添加值,超时设置
  • 原文地址:https://www.cnblogs.com/shenchuanchao/p/22993507/tenant-id-field-not-enough-three-defense-lines
  • 最新文章
  • 按键接到 STM32 后:GPIO 输入到底读到了什么
    一切都会回潮-写在一个职业周期的低处
    踩坑记:一次Nacos客户端偶发connect timed out全链路排查
    我用豆包 Seed-2.1-pro-0915 做了个「今天吃啥」,中午点菜这事终于不用纠结了
    npm 源切换:一条命令搞定
    ESP32-WROOM-32 设备管理页从零到上线
    Redis 数据结构:从五种基础到四种高级
    Windows 本地部署 PrismML Ternary-Bonsai-2-27B:RTX 5070 + CUDA 13.3 + llama.cpp Web UI 完整教程
    LangGraph 基于状态图构建工作流入门
    央国企大模型安全合规应用新标杆!中国电信天翼云推出央企可信Token专区!
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
小工具 小游戏
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1

京公网安备 11010502049817号