内容提要
实测TypeORM中PostgreSQL行级安全设置租户的六种方式:用dataSource.query()设会话级租户会泄漏到其他请求;事务内用注入的repository会取第二条连接,导致计数为0甚至连接池死锁。正确做法是每请求一个事务,首条语句设租户,所有查询走该事务的manager。
延伸解读
连接池复用导致租户泄漏
文章实测显示,用 dataSource.query() 设置会话级租户后,连接归还池中不会重置设置。并发30请求时,约一半请求读到其他租户数据。这是因为连接池复用连接,下一个请求可能拿到残留租户设置的连接。开发者需警惕:任何依赖连接状态的安全设置,都必须确保在请求结束时清理,或改用事务级设置。
注入Repository破坏事务一致性
在 dataSource.transaction() 内使用注入的 repository(如 NestJS 的 @InjectRepository)会从池中获取第二条连接,导致查询不在事务内。实测中计数为0,并发下连接池死锁。正确做法是从事务回调的 manager 获取 repository,确保所有查询走同一连接。这提醒我们:依赖注入的 repository 不自动绑定事务,需显式传递 manager。
无租户请求需显式设置空值
对于没有租户的请求(如健康检查),跳过 set_config 调用是危险的。实测中,若连接上残留了会话级租户设置,无租户请求会读到该租户数据。正确做法是显式设置租户为空字符串,例如 set_config('app.tenant', '', true)。这能确保策略中的 nullif 将空值转为 NULL,从而不返回任何行。
事务方案性能与安全兼顾
每请求一个事务,首条语句设置租户,所有查询走事务 manager,这种方案在顺序和并发测试中均未泄漏,且插入回滚正常。性能上,平均每请求0.42-0.47毫秒,与泄漏的会话级方案(0.35-0.42毫秒)接近,比 QueryRunner 加重置(0.54-0.61毫秒)更快。因此,该方案在安全性和性能上都是推荐选择。
Q&A
在 TypeORM 中设置 PostgreSQL 行级安全的租户,为什么用 dataSource.query() 设置会话级租户会导致数据泄漏?
因为 dataSource.query() 和后续的 repository 查询可能从连接池获取不同的连接。会话级设置(set_config 的第三个参数为 false)会保留在连接上,当连接返回池中时不会被重置,下一个请求如果拿到同一个连接,就会看到上一个租户的设置,导致数据泄漏。
在 TypeORM 事务中,为什么使用注入的 repository 会导致查询计数为 0 甚至连接池死锁?
注入的 repository(如 NestJS 中 @InjectRepository 获得的)不是绑定到当前事务的,它的查询会向连接池请求第二个连接。而事务已经占用了一个连接,当并发请求较多时,所有连接都被打开的事务占用,每个事务又在等待第二个连接,导致连接池死锁。同时,第二个连接上没有设置租户,所以计数为 0。
在 TypeORM 中,设置 PostgreSQL 行级安全租户的正确做法是什么?
正确做法是每个请求使用一个事务,在事务的第一条语句中设置租户,并且该请求的所有查询都通过该事务的 manager 进行。例如:dataSource.transaction(async (manager) => { await manager.query("select set_config('app.tenant', $1, true)", [tenantId]); return manager.getRepository(Invoice).count(); })。对于没有租户的请求,应将租户设置为空字符串 '',而不是跳过设置。
使用 QueryRunner 设置租户时,为什么必须在 release() 前重置租户?
因为 QueryRunner 使用的连接在 release() 后会返回连接池,如果不重置租户,会话级设置会保留在连接上,下一个请求可能拿到该连接并看到错误的租户数据。重置操作必须放在 finally 块中,确保即使发生错误也能执行。
在 TypeORM 中,为什么无法实现“每次从连接池获取连接时都设置租户”?
因为 TypeORM 1.1.2 没有提供在每次从连接池获取连接时运行并等待的钩子。afterConnect() 只在 DataSource 初始化时运行一次;订阅者只能看到查询和事务,看不到连接获取。底层 pg-pool 的 onConnect 也只在新物理连接建立时运行一次,其 acquire 和 release 事件不是 await 的。因此,无法在每次连接检出时自动设置租户。
在 TypeORM 中,使用事务 manager 设置租户的性能开销如何?
根据测量,在 3000 个顺序请求中,使用事务 manager 设置租户的平均耗时约为 0.42–0.47 毫秒,而使用 QueryRunner 并在释放前重置的耗时约为 0.54–0.61 毫秒。事务方式甚至比泄漏的会话级设置(0.35–0.42 毫秒)略慢,但性能接近。