XYGo Admin 多租户架构:租户专属表族为何比 tenant_id 字段更安全?

5 阅读3分钟

多租户的两种思路

做 SaaS 或集团型后台,多租户是绕不开的坎。市面上最常见的方案是在每张业务表里加一个 tenant_id 字段,查询时永远带 WHERE tenant_id = ?。这种"行级隔离"写起来简单,但隐患不少——哪天有人 JOIN 漏了条件,数据就串了。

XYGo Admin 选了一条不同的路:租户专属表族。平台管理员体系用 xy_admin_* 表,租户体系用 xy_tenant_* 表,两套表结构物理分离。

表级隔离到底长什么样

直接看表结构就能感受到差异:

平台表(管理员体系)         租户表(租户体系)
xy_admin_user               xy_tenant_admin
xy_admin_role               xy_tenant_role
xy_admin_menu               xy_tenant_menu
xy_admin_dept               xy_tenant_dept
xy_admin_post               xy_tenant_post

共享资源表(如附件表)则通过 tenant_id = 0 标记平台级数据,大于 0 归属对应租户。整个设计在数据库层面就一目了然:哪些是平台数据、哪些是租户数据,运维时不用猜。

五个维度看表级隔离的优势

1. 数据安全边界更清晰。 租户管理员、角色、菜单等核心数据跑在独立表里,从根本上杜绝了"忘加 WHERE 条件导致跨租户泄漏"这类事故。物理隔离的兜底能力不是代码评审能替代的。

2. 索引效率更高。 独立表的自增主键就是天然聚簇索引,不需要每张表都维护 (tenant_id, id) 联合索引。租户数据量越大,这个差异越明显。

3. 字段与功能独立演进。 租户菜单可以和平台菜单拥有不同的字段结构。比如平台端加一个"审计级别"字段,租户端的表完全不受影响,反之亦然。

4. 数据迁移与备份粒度更细。 导出 xy_tenant_* 相关表就能完整迁移一个租户,不需要从混合表中按条件抽取,运维效率高一个量级。

5. 诊断排错更直观。 线上出了问题,看表名就知道该查租户侧还是平台侧。

租户识别是怎么串起来的

请求进来后,TenantResolve 中间件按优先级解析租户身份:

  1. Header X-Tenant-Id(前端显式传,优先级最高)
  2. 域名匹配(查 xy_tenant.domain,适合独立域名场景)
  3. JWT 中的 TenantId(已登录用户的 Token 里自带)

解析结果为 0 表示平台级请求,不做租户过滤。整个中间件链是:

CORS → ResponseHandler → TenantResolve → TenantAdminAuth → DemoGuard → Controller

租户管理员使用独立的 JWT 签发逻辑(token.GenerateTenant / token.ParseTenant),与平台管理员 Token 互不冲突。租户登录走 /tenant/auth/login,查 xy_tenant_admin 表校验身份。

什么时候该用哪种方案

方案隔离粒度数据安全运维成本
独立数据库物理级最高
独立 Schema逻辑级
租户专属表族表级
tenant_id行级

如果你的项目租户数在几十到几百之间,且对数据隔离有硬性要求(金融、医疗、政务),租户专属表族是性价比最高的选择。租户数上万时,可能需要考虑独立数据库或 Schema 方案。

小结

多租户不是"加个字段就完事"的工程决策。XYGo Admin 的租户专属表族方案在安全性、性能和运维友好度之间找到了一个很好的平衡点。如果你也在基于 GoFrame + Vue3 做后台项目,这套设计值得参考。

更多细节可查阅 XYGo Admin 官方文档,项目已基于 MIT 协议开源。