SaaS系统中的tenant到底是什么 理解租户边界才能真正看懂多租户架构
第一次接触SaaS架构时,tenant这个词很容易让人产生误解。有人把tenant理解成一个用户,有人把它理解成一个公司,也有人认为只要给每个客户建立一个数据库就是多租户。实际上,这几个理解都只碰到了tenant的一部分。
在SaaS系统里,tenant更准确地说是一组共享同一个应用服务、但拥有独立业务上下文、数据边界、权限边界和配置空间的客户组织或业务实体。它可能对应一家企业、一个学校、一个机构、一个团队,甚至某些产品中的一个独立工作区。一个tenant下面通常还可以拥有多个user,因此tenant和user不是同一个概念。
理解这一点以后,很多SaaS架构设计就会变得容易理解。用户登录以后,系统真正需要解决的问题并不只是“这个人是谁”,还包括“这个人属于哪个tenant”“他在这个tenant里是什么角色”“这个请求能够访问哪些tenant的数据”,以及“这个tenant使用了什么套餐、配置和业务规则”。
tenant首先是业务边界,而不是数据库
假设一家企业SaaS平台有1000家公司注册。
公司A有50名员工,公司B有300名员工,公司C只有一个管理员。三家公司都使用同一个网站、同一套后端程序和同一套API,但它们的客户数据、员工账号、订单、文件、权限和配置不能混在一起。
这时候,公司A、B、C分别可以成为tenant。
每个tenant下面可以存在很多user。一个user可能属于一个tenant,也可能根据产品设计属于多个tenant。例如,一个咨询顾问可能同时被邀请进入多个企业工作区。
因此,典型的数据关系可能是:
tenant
├── users
├── roles
├── customers
├── orders
├── projects
└── settings
这里最重要的不是数据库里有没有一个叫tenant的表,而是系统中的每一个业务请求,都能够确定自己的tenant上下文,并且后续的数据访问始终受到这个上下文约束。
为什么SaaS特别需要tenant_id
最常见的多租户实现之一,就是多个tenant共享同一个数据库和表结构,在业务表中增加tenant_id。
例如:
orders
--------------------------------
id
tenant_id
customer_id
amount
status
created_at
假设订单1001属于tenant 15,订单1002属于tenant 28。
应用查询tenant 15的订单时,不能只写:
SELECT * FROM orders;
而应该让查询始终带有正确的租户条件,例如:
SELECT *
FROM orders
WHERE tenant_id = :tenant_id;
这个看起来非常简单,但它实际上是多租户系统最危险的地方之一。
如果某个API忘记加入tenant_id条件,一个客户就有可能查询到另一个客户的数据。
因此,多租户安全不能依赖程序员“记得每次都写tenant_id”。成熟系统会尽量把租户上下文、数据访问层、ORM作用域、数据库策略以及授权机制结合起来,让跨租户访问尽可能在架构层面变得困难。
tenant和user为什么不能混为一谈
一个典型的SaaS系统可能存在这样的结构:
Tenant A
Alice
Bob
Charlie
Tenant B
David
Emma
Tenant C
Frank
Alice、Bob和Charlie是用户,而Tenant A是他们共同工作的业务边界。
如果系统只设计user_id,而没有tenant概念,那么后面很多问题都会变得复杂。例如,同一个客户的管理员如何管理员工?同一个用户如何切换多个企业?不同企业能否拥有相同的用户名?一个用户在不同企业中是否可以拥有不同角色?
加入tenant之后,这些关系就清晰很多。
权限也通常需要同时考虑tenant和role。例如,一个人在Tenant A中可能是管理员,但进入Tenant B后可能只是普通成员。
所以真正的授权判断往往不是简单的“这个用户有没有权限”,而是“这个用户在当前tenant上下文中有没有执行这个操作的权限”。
多租户并不意味着每个客户一个数据库
这是理解SaaS架构时最常见的误区。
多租户系统大致可以采用几种不同的隔离模式。
第一种是共享数据库、共享表,通过tenant_id区分数据。这种模式通常具有较高的资源利用率,也比较容易统一部署和维护,但应用和数据库层面的租户隔离必须设计得非常严谨。
第二种是共享数据库、不同schema。不同tenant的数据拥有相对独立的schema边界,在隔离性和运营复杂度之间取得不同的平衡。
第三种是每个tenant独立数据库。这种方式能够提供更强的数据隔离,并且在某些企业客户、监管要求或者需要独立备份恢复的场景中非常有价值,但随着客户数量增长,数据库数量、升级、监控、备份和连接管理都会变得更加复杂。
因此,不能简单地说“真正的多租户必须一个客户一个数据库”。恰恰相反,大规模SaaS经常需要根据客户规模、合规要求、性能以及成本采用混合模式。
例如普通客户使用共享数据库,而大型企业客户使用独立数据库。这也是现实SaaS架构中非常常见的设计。
tenant隔离也不是简单的Docker隔离
原始理解中经常把tenant隔离直接和Docker、KVM、VLAN联系起来。
这些技术当然可以参与基础设施隔离,但它们并不是tenant概念的必要组成部分。
一套SaaS服务完全可以让1000个tenant运行在同一组应用服务器、同一套容器集群甚至相同的应用进程中。
系统依靠请求中的认证信息确定用户身份,再解析出tenant上下文,随后在应用、数据库、缓存、对象存储和权限系统中贯彻这个上下文。
这意味着,多租户的核心问题其实是“边界管理”。
数据库必须知道数据属于哪个tenant,缓存必须知道缓存键属于哪个tenant,对象存储必须知道文件属于哪个tenant,消息队列中的任务也应该携带正确的tenant上下文。
否则数据库虽然隔离了,缓存却可能把Tenant A的数据返回给Tenant B;数据库没有问题,对象存储权限却可能让其他客户下载文件。
缓存是多租户系统里非常容易出事故的地方
例如系统使用Redis缓存用户信息。
错误设计可能是:
user:10001
如果用户ID在整个系统中是全局唯一,这种设计可能没有问题。
但如果某些业务对象的ID只在tenant内部唯一,就必须考虑租户上下文。例如:
tenant:15:customer:1001
tenant:28:customer:1001
这两个customer_id虽然都是1001,但属于完全不同的tenant。
如果缓存键没有包含正确的租户边界,就可能出现严重的数据串租户问题。
类似问题还会出现在搜索索引、对象存储路径、临时文件、队列任务和后台Job中。
因此,多租户安全不能只检查SQL。
tenant上下文应该从哪里来
成熟SaaS系统通常会在用户完成认证之后建立租户上下文。
例如用户登录后,身份系统知道用户是谁以及他属于哪些tenant。用户访问某个企业工作区时,系统确定当前tenant_id,然后将这个上下文传递给后续业务逻辑。
一个请求可能包含:
user_id
tenant_id
role
permissions
但这里有一个非常重要的安全原则:tenant_id不能简单相信浏览器提交过来的值。
如果客户端直接发送:
tenant_id=28
服务器不能因为这个数字存在,就允许当前用户访问tenant 28。
服务器必须根据经过验证的身份、membership以及授权关系确认这个用户是否确实属于tenant 28。
否则,一个普通用户只需要把请求中的tenant_id从15改成16,就可能尝试访问另一个客户的数据。
这就是典型的跨租户授权漏洞。
业务规则也可能属于tenant
tenant不仅决定“看哪些数据”,还可能决定“这些数据应该怎样处理”。
例如一个企业SaaS产品允许不同客户设置自己的审批流程。
Tenant A规定采购订单超过5000美元必须经过经理审批。
Tenant B规定超过10000美元才需要审批。
那么业务逻辑就不能把5000美元写死在代码里,而应该根据当前tenant加载对应配置。
类似情况还包括税率、时区、货币、品牌Logo、邮件模板、用户权限、数据保留期限、功能开关以及套餐限制。
因此,tenant通常拥有自己的configuration。
这也是SaaS产品与普通网站最大的架构区别之一:同一套软件正在服务很多客户,但每个客户又拥有自己的业务环境。
套餐限制也是tenant的一部分
例如SaaS平台提供Basic、Professional和Enterprise三个套餐。
不同tenant可能拥有不同的用户数量限制、存储空间、API调用额度或者高级功能。
这时候系统判断的也不是简单的“这个user能不能使用某个功能”,而是当前tenant是否购买了相应能力。
因此,一个请求通常需要同时经过身份认证、tenant授权和产品能力检查。
例如:
用户身份
当前tenant
用户角色
tenant套餐
具体资源权限
这些条件共同决定最终是否允许操作。
这也是为什么成熟SaaS系统往往不会把权限简单写成数据库中的一个is_admin字段。随着产品复杂度增加,角色、权限、套餐、资源归属和tenant边界都会进入授权模型。
多租户真正困难的地方在于“所有地方都必须记住租户”
数据库查询需要tenant上下文。
Redis缓存需要tenant上下文。
文件存储需要tenant上下文。
搜索索引需要tenant上下文。
后台任务需要tenant上下文。
审计日志需要tenant上下文。
报表系统需要tenant上下文。
数据导出也需要tenant上下文。
甚至客服人员进入客户后台进行故障排查时,也需要明确记录当前正在访问哪个tenant。
因此,多租户并不是给数据库增加一个tenant_id字段就结束了。
它是一种贯穿整个系统生命周期的架构约束。
大型SaaS为什么经常采用混合隔离
随着客户数量增长,所有tenant都使用完全相同的数据库模式未必是最佳方案。
小型客户的数据量很小,可以共享数据库和表。
大型客户可能要求独立数据库。
某些监管严格的客户可能要求数据存储在特定区域。
还有一些客户可能要求独立加密密钥、独立备份策略或者更严格的访问审计。
因此,现代SaaS往往不会追求一种“所有客户都一样”的隔离方案,而是根据客户等级、数据敏感程度、性能需求和合规要求进行分层。
这也解释了为什么tenant概念比“数据库”更重要。
数据库只是承载数据的一种技术手段,而tenant才是业务上的边界。
多租户最危险的故障不是系统宕机,而是数据串租户
服务器宕机,用户通常知道系统出了问题。
但是如果一个API因为忘记tenant过滤条件,把A公司的客户名单返回给B公司,问题就严重得多,而且有时候很难立即被发现。
因此,多租户系统必须把跨租户访问作为重点安全测试对象。
测试人员应该主动测试修改tenant ID、修改资源ID、重复提交请求、直接调用内部API、访问其他租户的对象存储路径以及利用缓存键猜测其他客户数据等情况。
数据库层面的约束、应用层授权、API权限检查、缓存键设计、对象存储策略以及审计日志,都应该形成多层防护。
tenant这个概念真正值得工程师掌握的地方,不是记住一个英文单词,而是理解SaaS为什么必须建立“客户边界”。
一个user代表一个身份,一个role代表一组权限,而tenant代表一个独立的业务和数据上下文。多租户系统的核心工作,就是让同一套软件可以服务大量客户,同时保证每个客户看到的只是属于自己的数据、配置、资源和业务状态。
当你开始从tenant_id一路追踪到数据库查询、Redis key、文件路径、消息队列、后台Job、API授权和审计日志时,才算真正开始理解多租户架构。因为多租户从来不只是“数据库里多了一列tenant_id”,而是整个SaaS系统都必须始终知道:这个请求究竟属于谁。
SaaS 系统中的 tenant 到底是什么,理解这个概念才能真正看懂多租户
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
分享 Facebook | X | WhatsApp | LinkedIn
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP