java 数据库连接池配置租户权限的核心在于将租户标识(如 tenant_id)与物理连接或 sql 执行上下文绑定,通常通过自定义 datasource 装饰器、拦截器或动态数据源路由实现。具体做法是定义一个包含租户信息的线程局部变量(threadlocal),在获取连接前注入该信息,并在执行 sql 时自动附加租户过滤条件或切换 schema。这样做能确保每个请求仅在授权范围内操作数据,避免越权访问。

如何基于 ThreadLocal 实现租户上下文传递
通过 ThreadLocal 存储当前请求的租户 ID,可以在服务层设置上下文,在 DAO 层读取并应用。这种方式简单直接,适用于单线程模型下的 Web 请求处理,确保同一请求生命周期内租户信息一致。

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class TenantAwareDataSource implements DataSource {
private final DataSource targetDataSource;
private static final ThreadLocal<string> tenantContext = new ThreadLocal();
public TenantAwareDataSource(DataSource targetDataSource) {
this.targetDataSource = targetDataSource;
}
public static void setTenantId(String tenantId) {
tenantContext.set(tenantId);
}
public static String getTenantId() {
return tenantContext.get();
}
public static void clear() {
tenantContext.remove();
}
@Override
public Connection getConnection() throws SQLException {
String tenantId = getTenantId();
if (tenantId == null) {
throw new SQLException("Tenant ID not set");
}
Connection conn = targetDataSource.getConnection();
// 假设使用 PostgreSQL,通过 SET SESSION 参数传递租户
conn.createStatement().execute("SET app.current_tenant = '" + tenantId + "'");
return new TenantConnectionProxy(conn, tenantId);
}
// 其他 DataSource 方法省略...
}</string>
上述代码展示了如何包装标准 DataSource。当调用 getConnection() 时,它会检查 ThreadLocal 中是否存在租户 ID,若存在则获取底层连接并执行 SET 命令将租户信息存入会话级参数。后续所有 SQL 执行均可通过触发器或视图基于此参数进行过滤。测试时需先设置租户 ID,再获取连接,最后清理上下文以防内存泄漏。
如何在 MyBatis 中集成租户插件
在 MyBatis 框架中,可以通过实现 Interceptor 接口拦截 StatementHandler 的 prepare 方法,动态修改 SQL 语句以追加租户条件。这种方法无需修改业务 SQL,适合多租户 SaaS 场景,能统一处理数据隔离逻辑。

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class TenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
String tenantId = TenantAwareDataSource.getTenantId();
if (tenantId != null) {
// 简单示例:在 SQL 末尾追加 WHERE 条件
// 实际生产环境应使用更安全的 AST 解析器
String originalSql = handler.getBoundSql().getSql();
String modifiedSql = originalSql + " AND tenant_id = '" + tenantId + "'";
// 注意:此处仅为演示,实际需替换 BoundSql 中的 SQL
System.out.println("Modified SQL: " + modifiedSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可配置属性
}
}
该插件拦截 MyBatis 的 SQL 准备阶段,从 TenantAwareDataSource 获取当前租户 ID,并将其拼接到 SQL 语句中。虽然本例采用字符串拼接方式便于理解,但生产环境建议使用 JSQLParser 等库进行安全解析。注册此插件后,所有通过 MyBatis 执行的查询都会自动带上租户过滤条件,实现数据层面的多租户隔离。
当 ThreadLocal 未设置租户 ID 时会发生什么
如果未在请求上下文中设置租户 ID,TenantAwareDataSource 的 getConnection() 方法会抛出 SQLException,阻止连接获取。这确保了没有明确租户标识的请求无法访问数据库,防止数据泄露或未授权访问。开发者必须在过滤器或拦截器中保证每个请求都设置了有效的租户 ID,否则请求将被拒绝。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











