多租户到底怎么隔数据?Gauzy 隔离机制的实现真相
多租户到底怎么隔数据Gauzy 隔离机制的实现真相【免费下载链接】ever-gauzyEver® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co项目地址: https://gitcode.com/GitHub_Trending/ev/ever-gauzy多租户Multi-Tenancy是任何 SaaS 化业务系统都绕不开的命题几十上百家企业共用一套部署A 公司的发票、员工、项目数据绝不能泄露给 B 公司。开源 ERP/CRM/HRM 平台 Gauzyever-gauzy把这一命题直接做进了框架底层——它不靠业务代码里记得过滤来保证隔离而是让租户隔离成为 ORM 实体、服务基类与请求守卫三层协同的默认行为。本文从仓库源码出发拆解 Gauzy 的租户模型、隔离边界与写入防护并讨论这套机制对二次开发和系统性能的真实影响。多租户数据隔离的三种技术路线业界常见的多租户隔离方案大体分三类独立数据库Database-per-Tenant每个租户一个库物理隔离最彻底、恢复与扩容最灵活但连接池、迁移、备份成本随租户数线性上涨小型团队难以负担共享库 独立 SchemaSchema-per-Tenant同库不同 schema隔离性尚可但跨租户统计与 schema 管理复杂度高且多数 ORM 对动态 schema 切换支持一般共享库 共享表 租户列Shared Table / Row-level Isolation所有租户数据同表靠tenant_id列区分归属。成本最低、运维最简单但隔离完全依赖应用层每次查询都不忘带条件。Gauzy 选择的是第三条路线——共享表 租户列并且把带条件这件事从人的自觉变成了框架的默认。这样做的好处很直接Docker 一键拉起一套 PostgreSQL 就能服务多租户对自托管的中小企业场景最友好代价则是隔离责任全部压在框架层一旦某个查询漏掉租户条件就是全量数据泄露。Gauzy 正是围绕绝不漏掉这一目标设计了下面这套机制。租户模型两级隔离边界Gauzy 的租户隔离是两级的最外层是Tenant租户内层是Organization组织。一个租户下可以有多个组织日常业务数据发票、工时、候选人、任务几乎全部挂在组织之下而用户则直接挂在租户之下。先看租户根实体 Tenant它持有名称、Logo、标准工时等租户级配置并通过一对多关系下挂organizations、rolePermissions角色权限、featureOrganizations功能开关——角色的权限集也是按租户隔离的这意味着 A 租户调整权限不会影响 B 租户。真正起隔离作用的是两个抽象基类。TenantBaseEntity 是所有租户级实体的公共父类它定义了唯一的隔离外键export abstract class TenantBaseEntity extends BaseEntity implements IBasePerTenantEntityModel { MultiORMManyToOne(() Tenant, { nullable: true, onDelete: CASCADE }) tenant?: ITenant; IsUUID() RelationId((t: TenantBaseEntity) t.tenant) ColumnIndex() MultiORMColumn({ nullable: true, relationId: true }) tenantId?: ID; }注意两个细节tenantId上打了ColumnIndex()索引保证按租户过滤的查询走索引onDelete: CASCADE让租户删除时级联清理其全部业务数据。而 TenantOrganizationBaseEntity 在其上再扩展一层organizationId同样带索引与外键级联。用户User直接继承TenantBaseEntity组织Organization继承TenantBaseEntity绝大多数业务实体如 candidate、invoice、task 等 3000 多个核心文件都继承TenantOrganizationBaseEntity。于是整个数据模型天然形成一棵租户 → 组织 → 业务行的归属树。隔离边界从哪来RequestContext 与 JWT有了列条件从哪取答案在 RequestContext。它基于nestjs-cls的 AsyncLocalStorage在请求进入时把当前登录用户放入上下文并提供一系列静态访问器static currentTenantId(): ID | null { const user: IUser | null RequestContext.currentUser(); return user?.tenantId || null; } static currentOrganizationId(): ID | null { const user: IUser | null RequestContext.currentUser(); return user?.lastOrganizationId || null; }这里的用户来自 JWT 策略在请求头解析出的身份tenantId取自用户记录而非请求参数。Gauzy 在 bootstrap 中注册了全局AuthGuard所有路由默认要求登录也就是说没有合法身份的请求根本到不了服务层而服务层拿到的tenantId只来自服务端信任的用户对象。这正是不能信任客户端传参这一安全原则的落地——隔离条件永远由框架从身份推导而不是从 query/body 里读。读路径查询条件自动拼装隔离的最后一公里在 CRUD 服务层。绝大多数业务服务继承 TenantAwareCrudService它的注释写得很直白如果 RequestContext 中有用户就给所有查询过滤器加上 tenantId如果没有用户行为与普通 CrudService 完全一致。以findAll为例每次调用都会先经过findManyWithTenant()把用户身份翻译成查询条件private findConditionsWithTenantByUser(user: IUser): FindOptionsWhereT { return { ...(this.typeOrmRepository.metadata?.hasColumnWithPropertyPath(tenantId) ? { tenant: { id: user.tenantId }, tenantId: user.tenantId } : {}), ...this.findConditionsWithEmployeeByUser() } as FindOptionsWhereT; }这段代码有几个关键设计条件合并而非覆盖调用方传入的where会与租户条件合并findConditionsWithTenant中逐个展开数组形式的 where业务过滤逻辑不被破坏列存在性探测hasColumnWithPropertyPath(tenantId)动态判断实体是否有租户列让全局性实体如公开统计类天然跳过租户过滤员工级再收敛findConditionsWithEmployeeByUser会进一步叠加employeeId过滤——普通员工只能看到自己的数据除非拥有CHANGE_SELECTED_EMPLOYEE权限如管理者查看团队数据。TenantAwareCrudService还为此提供了withoutEmployeeFilter()逃生舱用引用计数的 AsyncLocalStorage 深度控制避免并发请求间的状态串扰。findOne、count、paginate、delete、softDelete等所有读/写查询入口都套用了同一套拼接逻辑保证从任何入口查条件都在。写路径自动打戳与越权防护查询要过滤写入更不能裸奔。create()在落库前会强制给实体盖上租户戳public async create(entity: IPartialEntityT): PromiseT { const tenantId RequestContext.currentTenantId(); await this.assertNotForeignRow(entity, tenantId); await this.assertNestedGraphNotForeign([entity], tenantId); return await super.create({ ...entity, ...(hasTenantColumn ? { tenant: { id: tenantId }, tenantId } : {}), // ...employeeId 逻辑同理 }); }即使调用方在请求体里塞了一个别人的tenantId也会被服务端身份覆盖——客户端永远无法通过构造 payload 把数据写进别的租户。同时TypeORM 的实体事件订阅器TenantOrganizationBaseEntityEventSubscriber会在beforeEntityCreate里根据tenantId/organizationId补齐关联对象保证外键关系完整。写路径上最值得细读的是assertNotForeignRow。它的动机在源码注释里讲得很清楚save()/create()携带 id 时会被 TypeORM 当作 upsert——按主键查、存在就 UPDATE而服务层只会把调用者的 tenantId 盖上去。如果请求体走私了一个其他租户的 id就可能发生改掉并重新归属别人数据的越权写入源码中明确提到了 GHSA-gwpq-mmw7-vx85 / GHSA-x4mv-fhwj-g3rp 这类漏洞类别。该防护在写入前用withDeleted: trueTypeORM/filters: falseMikroORM查一次目标行的真实租户归属不属于当前租户就直接抛ForbiddenException(The record belongs to another tenant)并且对无租户的空行也采取 fail-closed 策略。createMany/saveMany还提供了批量版本assertNotForeignRows一次查询校验所有 idassertNestedGraphNotForeign则把校验延伸到级联子对象堵住通过嵌套关系重新挂靠其他租户数据的旁路。请求层还有一道兜底闸门 TenantBaseGuard它比对请求头tenant-id、query 参数或 JSON body 中声明的租户与当前登录租户是否一致不一致直接拒绝。三处防线请求守卫、服务层过滤、写入防护叠加构成了前端传了也没用、服务层漏不了、写入抢不走的完整闭环。值得一提的还有delete上的谓词守卫assertCriteriaHasPredicate要求调用方必须给出明确条件防止出现delete({ employeeId: undefined })这类因注入的租户条件通过校验、却把整个租户数据误删的极端场景——隔离机制再强也防不住条件为空 全删的 SQL 语义陷阱。双 ORM 适配一套隔离逻辑两套引擎Gauzy 同时支持 TypeORM 与 MikroORM通过DB_ORM环境变量切换上面的基类全部用MultiORM*装饰器MultiORMColumn、MultiORMManyToOne、MultiORMEntity编写仓库中数百个实体均如此声明。这意味着租户隔离逻辑不依赖特定 ORM 的私有 API——TenantAwareCrudService内部对两种 ORM 的查询写法分别适配如上面assertNotForeignRow的 switch 分支上层业务代码完全无感。对二次开发者而言新增实体只要继承对应基类并沿用MultiORM装饰器就能免费获得与核心实体一致的隔离能力无论底层切到哪个 ORM。对二次开发与性能的影响二次开发视角租户隔离被做成了继承即得的默认能力业务侧几乎不需要感知。但这也意味着三条纪律必须遵守第一自定义实体应继承TenantBaseEntity或TenantOrganizationBaseEntity而不是裸继承BaseEntity否则数据将游离于隔离体系之外第二自定义服务应继承TenantAwareCrudService以获得自动过滤与自动打戳确需跨租户操作时显式使用saveWithoutEnrichment之类的逃生通道而不是偷偷绕过第三新增的全局性非租户级实体要能接受hasColumnWithPropertyPath(tenantId)探测自动跳过租户过滤的行为避免误伤。反过来对想深度定制隔离边界比如让某个模块跨组织共享的团队这套基于基类的设计意味着你只需要在该模块的服务里覆盖条件拼装逻辑而不必动全局框架。性能视角共享表方案最大的性能隐患是租户列无索引导致全表扫描。Gauzy 通过两点缓解tenantId/organizationId列上强制ColumnIndex()且过滤条件会同时命中tenantId与organizationId两列查询计划可以走复合索引路径加上外键CASCADE让数据清理随租户删除自动完成避免了孤儿数据长期堆积拖慢扫描。当然这种行级过滤仍会在每个查询上叠加额外谓词租户数量极大、单表数据量极高时可以考虑按租户做表分区partition by tenantId或升级到独立 schema 方案——Gauzy 的实体设计保留了这种演进空间因为隔离外键从第一天就是显式建模的。小结Gauzy 的多租户隔离是一个典型的应用层行级隔离工程范本实体基类定义归属结构RequestContext 从受信任身份推导租户CRUD 基类在读写两端强制拼装与校验请求守卫兜底双 ORM 适配保证一致性。这套机制最大的价值不在于某一行代码而在于把数据隔离从开发者的心智负担中剥离出来变成框架级默认行为——对于打算在开源 ERP 上做多租户二开的团队理解这条链路就等于掌握了这个系统数据安全的地基。【免费下载链接】ever-gauzyEver® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co项目地址: https://gitcode.com/GitHub_Trending/ev/ever-gauzy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

从理论到实战:深度解析MCP模型上下文协议的应用与实践|TaoToken统一Key接入指南

从理论到实战:深度解析MCP模型上下文协议的应用与实践|TaoToken统一Key接入指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:07:31 阅读更多 →
Eclipse中搭建Spring框架全流程:从环境配置到IoC与DI实战

Eclipse中搭建Spring框架全流程:从环境配置到IoC与DI实战

刚入行那阵子,我花了不少时间折腾Spring。那时候网上资料远不如现在丰富,大部分教程都是基于IDEA的,用Eclipse的教程要么太老,要么讲得云里雾里。做Java开发这些年,从SSH到SSM再到Spring Boot,Spring这个框…

2026/10/12 2:07:34 阅读更多 →
Linux history命令全解析:从存储原理到实战技巧

Linux history命令全解析:从存储原理到实战技巧

1. history 命令到底是什么很多 Linux 新手第一次接触history命令时,觉得它就是个“查聊天记录”的小工具,敲一下回车,把自己最近执行过的命令列出来。这个理解没错,但远远不够。history是 Bash 等 Shell 内置的历史记录功能。你在…

2026/10/12 2:07:37 阅读更多 →

最新新闻

数据结构 - > 排序算法

数据结构 - > 排序算法

1. 排序的概念1.1 常见的排序算法1.2 排序算法的评价指标复杂度:评价排序算法的第一大指标就是时间复杂度和空间复杂度,它衡量算法的时间效率和空间效率。稳定性:假定在待排序的数据元素中有两个元素 Ri 和 Rj,它们对应的关键字为…

2026/10/12 3:39:10 阅读更多 →
ccg-workflow Shell 技能指南:Bash 脚本自动化、系统管理与多模型协作实战

ccg-workflow Shell 技能指南:Bash 脚本自动化、系统管理与多模型协作实战

【免费下载链接】ccg-workflow 多模型协作工作流引擎 — /ccg:go 一个命令,AI 自动分析意图、选择策略、编排 Codex Gemini Claude 协作执行 项目地址: https://gitcode.com/gh_mirrors/cc/ccg-workflow 点击查看 免费下载 导读 本文基于 ccg-workfl…

2026/10/12 3:39:10 阅读更多 →
2026年软件测试趋势:AI Agent、质量内建与可观测性重塑质量保障

2026年软件测试趋势:AI Agent、质量内建与可观测性重塑质量保障

做测试这行,每年年底都在猜明年的技术方向,但2026年这次不太一样。我最近和不少测试负责人、开发团队聊下来,大家最焦虑的已经不是又冒出了什么新工具,而是整个质量体系正在被 AI 和平台工程重构,很多沿用多年的测试方…

2026/10/12 3:39:10 阅读更多 →
netdxf实战:DXF文字注释与尺寸标注的创建与修改

netdxf实战:DXF文字注释与尺寸标注的创建与修改

接触过DXF开发的人应该都有这种感觉:画直线、画圆、画多段线都属于“基本功”,真正让图纸变得可读、可传递设计意图的,是文字注释和尺寸标注。这一篇是整个netdxf系列里我比较想写的一篇,因为注释和标注的处理逻辑和普通几何实体完…

2026/10/12 3:39:10 阅读更多 →
文华财经主升浪买点指标公式拆解:多条件共振识别趋势启动

文华财经主升浪买点指标公式拆解:多条件共振识别趋势启动

1. 文华财经主升浪买点指标的实战拆解做期货日内或者波段的朋友,应该都听过“主升浪”这个词。行情走主升浪的时候,速度最快、幅度最大,但也是最难拿得住的一段。很多朋友在文华财经软件里翻遍了各类指标公式,要么信号滞后&#x…

2026/10/12 3:39:10 阅读更多 →
ppt-master 的 IBM 品牌身份预设解析:从 Carbon Blue 设计规范到可执行的 design_spec

ppt-master 的 IBM 品牌身份预设解析:从 Carbon Blue 设计规范到可执行的 design_spec

AI 技能人工智能 【免费下载链接】ppt-master AI 把任意文档生成真正可编辑的 PowerPoint —— 原生形状与动画、演讲者备注可合成音频旁白、还能参考你自己的 .pptx 模板,而不是一张张图片 何雨果出品 项目地址: https://gitcode.com/hugohe3/ppt-master 点击查看…

2026/10/12 3:38:09 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →