multi-tenancy.md 3.21 KB

多租户与产品版本

xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一套元数据服务多个客户。框架通过三条相互独立的轴来强制租户边界,它们作用在不同层级。

真实模块中的贯穿示例见切片 2。本页是可从任何地方链接过来的规范摘要。

三条轴

携带位置 粒度 事实来源
sBrandsId(加工商ID) 几乎每条业务行 每行 用户 session(UserInfo.getsBrandsId()
sSubsidiaryId(子公司ID) 几乎每条业务行 每行 用户 session
sVersionFlowId(版本流程ID) gdsmodule 每模块 用户所属产品版本(对应 sisversionflow

实时 DB 中每行作用域非常普遍:sBrandsId 出现在 1,009 张表 / 视图上,sSubsidiaryId 出现在 1,008 张表 / 视图上。几乎所有业务数据表和框架元数据表都带有它们。

每模块 gating(sVersionFlowId)只出现在 3 张表上,并且只有 gdsmodule 是实时表,另外两张是备份。因此版本 gating 是模块发现阶段的一次性过滤,不是每行检查。

如何强制执行

每个已认证 REST 端点在请求到达后都会运行同一行:

RequestAddParamUtil.me().addParams(params, userInfo);

xlyPersist/.../RequestAddParamUtil.java。这个 56 行工具类从 UserInfo 中取出用户身份,并向请求 params map 写入 16 个 key(sBrandsIdsSubsidiaryIdsBrIdsSuIdsLoginIdsIpAddresssComputeNamesUserIduserIdsLanguagesUserTypesUserNamesMakePersonsTeamIdsMachineId,以及 CURRENT_USER_LOGIN_TYPE)。xlyApi 在 xlyApi/.../api/util/RequestAddParamUtil.java 中有几乎相同的 57 行副本。任何下游 MyBatis 查询只要引用 #{sBrandsId}#{sSubsidiaryId},就会自动受租户作用域约束。前端不能影响这些值;它们来自服务端 session,并通过 @CurrentUser 参数解析器进入。

失效模式

忘记按 sBrandsId 过滤的查询会返回所有租户的行,这是灾难性数据泄漏。需要关注三处:

  1. 运行时拼动态 SQL 的存储过程,开发者必须记得注入 sBrandsId。代码库中基本一致,但数据库不会强制。
  2. 数据库视图。 几乎所有 viw_* 视图都会从主基础表带出租户列,但手写视图如果漏掉这些列,就可能造成按行泄漏。维护审计脚本应标记此类视图。
  3. 保存端点addUpdateDelBusinessData),它允许前端在 payload 中直接提供 sTable。如果运行时不校验该表是否属于表单授权范围,这就是权限提升面。见切片 1

为什么这个实时 DB 看起来很小

xlyweberp_saas_ai 只有一个品牌(sBrandsId = '1111111111')、一个子公司,以及一个有数据的版本(8S_001 / 基础版)。多租户机制已经接好,但这里没有充分使用。生产租户会有几十个品牌、每个品牌下几十个子公司,以及多个版本;设计通过行数扩展,而不是通过代码分支扩展。