Chris van Eijk:Spring Boot 中的 Hibernate 多租户与 PostgreSQL 行级安全

Chris van Eijk:Spring Boot 中的 Hibernate 多租户与 PostgreSQL 行级安全

💡 原文英文,约2200词,阅读约需8分钟。
📝

内容提要

实测Spring Boot中为PostgreSQL行级安全设置租户的四种方式:DataSource包装器和拦截器在并发下会泄露其他租户数据;@Transactional方法首条语句和Hibernate连接提供者能隔离租户。推荐在事务内用set_config设置租户,性能开销可忽略。

🔎

延伸解读

连接池复用是租户泄漏的根源

文章实测显示,DataSource包装器和拦截器在并发下会泄漏其他租户数据,根本原因在于HikariCP连接池会复用数据库会话。当请求未设置租户时,连接可能保留前一个请求的租户设置,导致读取到其他租户的数据。拦截器在30并发下,即使请求设置了租户,也可能因连接被其他请求修改而读到错误数据。这提醒开发者,任何在连接池之外设置会话级租户的方案都不可靠。

事务内设置租户是可靠方案

在@Transactional方法内使用set_config(…, true)设置租户,能确保设置仅在当前事务内有效,事务结束即清除,避免连接复用带来的污染。文章测试中,该方法在顺序和并发请求下均未出现租户泄漏,且性能开销可忽略。但需注意,事务外的查询不会自动应用租户设置,可能返回空结果或触发RLS策略拒绝,这属于可察觉的错误而非静默泄漏。

Hibernate提供者的适用边界

Hibernate的MultiTenantConnectionProvider在每次获取连接时设置租户,并在释放时重置,能有效隔离通过Hibernate执行的查询。然而,它仅覆盖Hibernate管理的连接,事务外的JdbcTemplate调用会从池中获取独立连接,不受租户设置影响。若未在释放时重置,还可能反向泄漏。因此,该方案适合所有查询都经由Hibernate的场景,混合使用需谨慎。

RLS与Hibernate过滤器的本质区别

Hibernate的@TenantId注解通过生成SQL时添加过滤条件实现多租户,但仅对Hibernate生成的语句有效。原生SQL、JdbcTemplate或直接连接数据库的报表工具会绕过该过滤器,看到全部数据。而PostgreSQL行级安全(RLS)由数据库强制执行,无论查询来自何处都受策略约束。因此,RLS提供了更底层的安全保障,尤其适合混合访问路径的应用。

❓

Q&A

在Spring Boot中为PostgreSQL行级安全设置租户,有哪几种常见方式?

文章实测了四种方式:1. 包装DataSource,在连接离开池时设置租户;2. 使用HandlerInterceptor在控制器运行前设置;3. 在@Transactional方法的第一条语句中设置;4. 使用Hibernate的MultiTenantConnectionProvider。

为什么在Spring Boot中使用DataSource包装器或拦截器设置租户会导致数据泄露?

因为HikariCP在连接返回池时不会重置自定义设置,导致后续请求可能读取前一个租户的数据。拦截器在并发下还会因为连接被其他请求使用而返回错误租户的数据。

在@Transactional方法中如何正确设置租户?

在@Transactional方法的第一条语句中执行set_config('app.tenant', :tenant, true),将租户设置为事务本地。这样设置只在当前事务内有效,事务提交后自动清除,且同一事务内的JdbcTemplate查询也能看到相同租户。

Hibernate的MultiTenantConnectionProvider如何工作?它覆盖哪些查询?

Hibernate在获取连接时调用getConnection(tenant),在其中执行set_config('app.tenant', ?, false)设置租户,并在releaseConnection时重置。它覆盖所有通过Hibernate获取的连接上的查询,包括Hibernate自身语句和@Transactional方法内的JdbcTemplate调用,但不覆盖事务外直接使用JdbcTemplate的查询。

Hibernate的@TenantId注解能否替代PostgreSQL行级安全?

不能。@TenantId只是Hibernate在生成的SQL中添加过滤条件,对于原生查询、JdbcTemplate调用或直接连接数据库的报表,它不会生效,这些查询会看到所有租户的数据。行级安全由数据库强制执行,能覆盖所有查询。

在事务内设置租户的性能开销如何?

性能开销可忽略。实测平均每请求耗时在0.55-0.92毫秒之间,不同方式之间的差异小于运行间的波动(约0.35毫秒),事务本身没有带来可测量的额外开销。

🏷️

标签

➡️

继续阅读