oracle多租户必须用vpd而非aop拦截sql,因aop字符串拼接易被order by、子查询、union等绕过导致泄露;vpd在内核层动态改写所有访问路径sql,连dba直连也无法绕过,spring只需通过connectioncustomizer安全注入并及时清理租户上下文。

为什么不能直接用AOP拦截SQL做租户过滤
Oracle本身不支持像MySQL那样用WHERE tenant_id = ?自动拼接全局条件——AOP在MyBatis或JPA执行前织入SQL,本质是字符串操作,极易被绕过或破坏语法。比如ORDER BY后注入、子查询漏过滤、UNION ALL跨租户拼接,都会导致数据泄露。这不是“加个切面就能解决”的问题,而是架构层面的误用。
Oracle多租户必须依赖VPD(Virtual Private Database)
Oracle原生的行级安全机制VPD才是正确解法:它在数据库内核层动态改写SQL,所有访问路径(包括触发器、DB Link、PL/SQL包)都强制生效,连DBA直连都无法绕过。Spring Boot只需确保每次会话绑定租户上下文,其余交给Oracle。
- 启用VPD前,先创建策略函数:
CREATE OR REPLACE FUNCTION tenant_policy(owner VARCHAR2, objname VARCHAR2) RETURN VARCHAR2 IS BEGIN RETURN 'tenant_id = SYS_CONTEXT(''TENANT_CTX'', ''ID'')'; END; - 将策略绑定到表:
DBMS_RLS.ADD_POLICY('SCOTT', 'CUSTOMERS', 'tenant_policy', 'SELECT,INSERT,UPDATE,DELETE', 'SCOTT', 'tenant_policy', TRUE); - Spring Boot中用
DataSource的setConnectionInitSqls设置会话上下文:ALTER SESSION SET CURRENT_SCHEMA = tenant_001或DBMS_SESSION.SET_CONTEXT('TENANT_CTX', 'ID', ?)
Spring Boot如何安全传递租户ID到Oracle会话
不能靠HTTP Header传参后直接拼进SQL,必须通过Connection生命周期控制。推荐用HikariCP的connectionInitSql + ThreadLocal组合:
- 定义
TenantContext.getTenantId()从Header/Token提取租户标识,存入ThreadLocal - 配置Hikari连接池时,设置
connection-init-sql: CALL DBMS_SESSION.SET_CONTEXT('TENANT_CTX', 'ID', '${tenant.id}')—— 但注意,这里不能硬编码${tenant.id},需用自定义ConnectionCustomizer - 实现
HikariConfig.setConnectionCustomizer(),在onConnect回调中调用connection.prepareCall("CALL DBMS_SESSION.SET_CONTEXT(?, ?, ?)").execute(),参数从TenantContext.getTenantId()取 - 务必在请求结束时调用
TenantContext.clear(),否则线程复用会导致下个请求继承错误租户上下文
为什么AbstractRoutingDataSource在这里反而多余
如果你已经用VPD做了行级隔离,再配AbstractRoutingDataSource切不同租户库,等于叠床架屋。Oracle VPD天然支持单库多租户,而AbstractRoutingDataSource适用于“独立数据库”方案(每个租户一个DB实例)。混用会导致两个问题:
- 事务跨数据源失效:
@Transactional无法覆盖多个DataSource,而VPD方案本就只用一个数据源 - 连接池污染:不同租户的连接被混在同一个池里,
SET_CONTEXT可能被覆盖,引发租户ID错乱
真正关键的不是“怎么切”,而是“租户上下文何时注入、何时清除、是否被所有SQL路径捕获”——Oracle的VPD机制已经覆盖了全部路径,Spring侧只需稳稳托住会话上下文那一瞬。











