SaaS 多租户数据隔离:MyBatis-Plus 多租户插件的底层原理与实战

绝对不能把防止数据越权的希望寄托在程序员的细心上。深入拆解MyBatis-Plus底层拦截器,利用AST语法树强行注入租户枷锁,彻底焊死串户漏洞。

在构建省级统一心理云平台时,系统往往需要同时为成百上千所学校提供服务。这就不可避免地涉及 B 端 SaaS 架构的核心议题:多租户(Multi-Tenant)数据隔离。

最底层的隔离方式是为每个学校单独建立一个数据库实例(物理隔离),但这种方案硬件成本高,且随着租户数量增加,运维压力会呈指数级上升。目前业界更主流的选择是逻辑隔离:将所有学校的数据存储在同一张数据表中,通过一个额外的字段 tenant_id(租户或学校 ID)来进行区分。

然而,逻辑隔离存在一定的安全隐患。如果开发人员在编写 SQL 语句时出现疏漏,忘记在 WHERE 条件中附加 tenant_id = 对应学校ID(例如误写成 SELECT * FROM student WHERE age = 15),那么这所学校的用户就可能看到整个平台所有 15 岁学生的数据。这种跨租户的数据越权查询,在 SaaS 系统中属于严重的安全事件。

为了从根本上规避人工疏忽带来的风险,在架构设计上引入自动化的 SQL 拦截机制是一条可行的路径。

引入底层拦截机制

将系统安全性寄托于代码规范审查是不够的。更可靠的做法是在执行层对 SQL 语句进行动态干预。

在 Java 生态中,通过 MyBatis-Plus 提供的拦截器(Interceptor)功能,我们可以在 JDBC 执行之前的阶段,捕获业务代码生成的原始 SQL 语句。

假设业务代码编写了一段基础的查询逻辑:
SELECT id, name FROM student

当这句 SQL 进入 MyBatis-Plus 的拦截链路时,多租户插件会利用 JSqlParser 这一 SQL 解析工具,将 SQL 字符串转换为抽象语法树(AST)。随后,插件会在语法树的 WHERE 条件节点中,动态追加当前上下文的租户 ID。

经过处理后,实际发送给数据库执行的语句变成了:
SELECT id, name FROM student WHERE tenant_id = 10086

对上层业务透明

这种在底层改写 SQL 的操作,对业务研发工程师来说是完全透明的。开发人员可以将精力集中在业务逻辑实现上,而不需要在成百上千条查询或更新语句中手动维护 tenant_id 过滤条件。

这套机制同样适用于多表关联查询。如果业务代码包含 JOIN 操作:
SELECT s.name, c.classname FROM student s JOIN class c ON s.classid = c.id

拦截器会解析出语句涉及的所有表名,并分别为它们添加租户过滤条件:
SELECT s.name, c.classname FROM student s JOIN class c ON s.classid = c.id WHERE s.tenantid = 10086 AND c.tenantid = 10086

针对公共表的白名单放行

在实际的平台业务中,部分数据表是全省乃至全局共享的,比如标准的“抑郁量表题库表”。这些表本身并不包含 tenantid 字段。针对此类情况,插件配置支持声明忽略规则(Ignore Table List)。橙星云技术团队在落地这套方案时,将类似 sysscale_question 的公共表名加入白名单,当拦截器匹配到这些表名时,将直接放行,不做任何 SQL 改写。

在复杂的 B 端数据架构中,利用 ORM 框架提供的底层拦截器与 AST 语法树操作来实现租户数据隔离,不仅减少了业务代码的侵入性,更能以技术手段建立起一道稳定可靠的数据安全防线。

Leave a Reply

Your email address will not be published. Required fields are marked *