多租户不只是 tenant_code:联犀的数据权限分层模型
约 3347 字大约 11 分钟
2026-05-16
提到多租户,很多系统的第一反应都是 tenant_code。这当然没有错。无论是共享库、共享 Schema 还是共享表方案,只要是 SaaS 平台,租户标识通常都是最基础的隔离锚点。但如果一个系统真正进入复杂业务阶段,只靠 tenant_code 很快就会不够用。因为它只能回答“这条数据属于哪个租户”,却回答不了“同一租户里的不同用户应该看到哪些项目、哪些区域、哪些设备,以及这些限制如何进入查询链路”。
联犀的数据权限设计,正是围绕这个问题展开的。它没有把多租户等同于一条数据库过滤条件,而是把数据权限拆成三个层次:基础隔离层、用户上下文层、查询裁剪层。这样设计的核心原因,不是为了把架构讲得更复杂,而是因为 SaaS 与 IoT 叠加之后,权限问题天然就是分层问题。如果试图用单一机制吃掉所有场景,最后一定会在业务侧到处补口子。
为什么只靠 tenant_code 很快会失效
很多多租户系统在早期都能靠 tenant_code 运转良好,因为那时的业务边界通常还比较粗。不同租户之间互不可见,已经覆盖了大部分需求。但随着平台深入业务现场,问题会迅速变得不一样。以 IoT 场景为例,同一个租户内部通常还会继续划分项目、区域、设备组、角色和岗位。对用户来说,最常见的权限问题不再是“能不能看别的租户的数据”,而是“能不能看本租户里另一个项目、另一个区域、另一批设备的数据”。
如果这时还把多租户理解为单一租户字段过滤,系统就会开始出现两类典型失控。第一类是业务逻辑层面失控。每个接口都会顺手加一点项目判断、区域判断、角色判断,久而久之查询和权限完全缠在一起。第二类是查询能力层面失控。统计接口、报表接口、聚合接口往往无法复用统一权限逻辑,只能一条条手写 where 条件,最终既难维护也难扩展。
这就是为什么联犀没有把多租户问题停留在存储模型层面,而是继续向上分层。租户隔离当然仍然重要,但它必须被放在更完整的数据权限链路里理解。否则系统看似实现了多租户,实际上只是把更复杂的权限问题推迟到了业务高峰期再集中爆发。
第一层:基础隔离层,解决“数据天然属于谁”
联犀的数据权限模型第一层,是基础隔离层。这一层回答的问题最直接:在默认情况下,系统应该天然把哪些数据当作当前上下文可见的边界。这里的关键做法不是每次查询手动拼 tenant_code = ?,而是通过共享数据类型和统一读写语义,把租户边界下沉到更底层的持久化约束里。
这样做的好处很明显。第一,租户隔离不再依赖每个业务开发者记得手写过滤,而是成为默认语义。第二,系统能够基于不同资源类型定义不同的租户读写规则,而不只是“一刀切按当前租户过滤”。联犀对普通租户数据、公共共享资源、平台保留资源采用了不同的租户语义,正说明多租户不是只有一个字段,而是一套带业务含义的边界模型。
例如,有些资源天然只属于当前租户,有些资源允许在当前租户和公共租户之间共享读取,还有些资源只希望由平台侧持有并控制是否暴露。把这些差异直接沉淀到共享数据类型里,比把判断留到每个查询接口临时处理更稳定。它的本质价值在于:系统先明确“默认可见边界”,再让上层权限机制在这个边界之上继续裁剪,而不是从零开始讨论每条数据该不该露出来。
基础隔离层的目标因此不是解决所有权限问题,而是建立一个可靠的默认面。只要这层足够稳定,后续新增业务能力时,团队至少不需要从“租户是否串数据”这种最低层问题重新开始担心。
第二层:用户上下文层,解决“同一租户里谁能看哪一块”
只有租户隔离,系统仍然无法回答租户内部的细粒度权限问题。联犀的第二层是用户上下文层,它的职责不是再做一次数据库过滤,而是把项目、区域、角色、管理员身份以及当前请求相关的业务范围,组织成后续链路都可以复用的权限输入。
这一层之所以重要,是因为租户内权限不是天然存在于数据库表结构里的,它更像是用户与业务资源之间的关系网络。项目授权决定用户是否能进入某个项目,区域授权决定其是否能继续查看某个子树或某批设备,管理员标记决定某些用户是否拥有更宽的越权边界。这些信息如果不在请求早期就被稳定组织起来,后续每个模块都会倾向于重新做一次权限解释。
联犀把这类信息放进统一用户上下文,并沿着 HTTP、RPC、消息总线继续透传,目的就是避免权限语义在链路中反复丢失或反复重建。平台只要进入了查询、推送、控制等后续阶段,就应该已经知道当前请求大致处于什么权限边界内,而不是每层都再去理解一遍 token 和角色。
从架构角度看,这一层最重要的意义是把“身份”和“范围”绑定起来。租户字段只描述身份归属,用户上下文则开始描述这个身份在业务域内的可见范围。没有这一层,后面的查询裁剪就缺少足够的输入;只有这一层,仍然还不够,因为这些范围信息最终还必须被翻译成真正可执行的查询条件。
第三层:查询裁剪层,解决“权限如何真正进入查询”
很多权限系统的问题,不是没有身份,也不是没有上下文,而是这些信息最后没能以统一方式进入查询层。开发者往往只能在具体接口里手工拼装条件,导致权限逻辑和业务逻辑高度耦合。联犀在这一点上的处理,是把查询权限收口到配置与 Hook 组成的查询裁剪层。
这一层的核心思想是,查询系统不只是执行 SQL 或时序语句,它还应该理解一部分权限约束。租户过滤是否自动启用,软删除字段是否统一处理,某类查询是否需要预置过滤条件,某些复杂场景是否交给 Hook 动态补充,这些都属于查询裁剪层的职责。它的重点不在于“做一个万能查询引擎”,而在于让权限翻译尽可能离查询入口更近、离业务接口更远。
联犀的数据服务里,查询配置与 dataFilter Hook 的组合就体现了这种思路。配置负责承接稳定的、结构化的过滤规则,例如租户过滤和固定前置条件;Hook 则负责承接那些明显依赖业务上下文的动态裁剪,例如项目范围、区域树范围、设备集合范围。这样一来,权限从用户上下文进入查询,不再只能靠每个接口自己展开,而是被收敛到一套统一的翻译入口。
这层设计的价值非常大。首先,它让普通查询和复杂查询能共享一套权限输入模型,而不是各写各的。其次,它让“新增一个权限场景”更多变成配置和扩展点问题,而不是业务代码复制问题。最后,它使得权限边界开始具备可审视性。团队可以明确知道某个查询受哪些配置和哪些 Hook 影响,而不是在一堆逻辑分支里追踪条件究竟是从哪里加进去的。
分层之后,多租户与业务权限终于不再互相替代
很多系统之所以在权限设计上反复返工,一个重要原因是它们总想用某一层机制替代另一层机制。有人希望 tenant_code 足够强大,顺手解决项目和区域问题;有人希望角色系统足够灵活,顺手承担数据隔离问题;也有人希望查询层足够聪明,自动推断所有业务范围。这些想法都很诱人,但实际效果往往是每一层都做了不属于自己的事,最后谁都做不好。
联犀的数据权限分层模型更务实。基础隔离层只负责兜住租户边界,不试图解释租户内部的业务关系;用户上下文层负责把项目、区域、角色这类业务权限组织起来,不直接替查询层生成所有语句;查询裁剪层负责把这些权限翻译成查询条件,但不重新定义租户和业务资源的来源。每一层都有清晰职责,也因此更容易独立演进。
这种分层还有一个直接收益,就是系统面对“公共资源”和“平台资源”这类特殊语义时更从容。公共模板、共享技能、平台侧保留配置,本来就不适合被简单视为某个租户私有数据。只有当基础隔离层本身支持不同租户语义,且上层权限机制继续在其之上收口,系统才能同时兼顾共享能力与隔离安全,而不是在两者之间二选一。
这套模型的真正意义,是避免接口级权限拼装失控
从工程实践角度看,分层模型最大的收益,并不只是理论上更完整,而是它能显著减少接口级权限拼装的失控概率。因为一旦没有统一分层,所有权限问题最终都会回落到业务接口:这个列表多加一个项目过滤,那个详情多补一个区域判断,另一个报表再拼一段角色条件。短期确实能解决问题,长期却会让权限成为最难复用、最难测试、最难解释的一块基础设施。
联犀把多租户和数据权限拆成三层,本质上是在提前阻止这件事发生。开发者在实现业务接口时,不必每次都从租户、项目、区域开始重建自己的权限世界观,而是优先复用底层隔离、上下文范围和查询裁剪的既有能力。系统因此可以把权限复杂度集中治理,而不是把复杂度分发给每个接口各自承担。
多租户真正难的,不是隔离,而是分层
如果要用一句话概括联犀这套设计的经验,那就是:多租户真正难的地方,从来不只是让数据带上 tenant_code,而是让系统在租户隔离、业务范围和查询落地之间形成稳定分工。前者只是一个必要前提,后者才是平台在复杂业务里能不能长期维持秩序的关键。
很多系统在介绍多租户时,会重点讨论数据库方案、表结构策略和部署成本。这些当然重要,但对真实业务系统来说,更容易把团队拖垮的,往往是“权限到底该在哪一层解决”这个问题。联犀的数据权限分层模型给出的答案很明确:租户边界下沉到底层,业务范围组织在上下文,查询收口放在统一裁剪层。只有这样,系统才能既保持隔离,又不把所有权限都写进每一个业务接口里。
所以,多租户确实从 tenant_code 开始,但绝不能停在那里。真正成熟的多租户平台,最终比拼的不是谁更早加上这个字段,而是谁能围绕它建立起一套不会随着业务增长而持续失控的权限分层体系。
更新日志
2026/5/18 10:48
查看所有更新日志
43ef3-docs(blog): 新增后端与架构系列技术博客 18 篇,更新原有 7 篇于
