Node连接池与PostgreSQL search_path:多租户连接绑定问题
多租户场景下PostgreSQL连接池的schema切换与连接绑定问题
不确保同一请求使用同一连接会引发的问题
是的,会引发严重的多租户数据隔离问题。因为PostgreSQL的search_path是连接级别的配置,连接池中的连接是被多个请求复用的:
- 假设请求A获取连接后设置
search_path = tenant_a,操作完成后将连接放回池; - 后续请求B复用这个连接,如果没有重新设置
search_path,就会默认访问tenant_a的schema,导致请求B读取/修改不属于自己租户的数据,完全破坏多租户的隔离性。
确保同一客户端请求使用同一连接的方案
1. 请求生命周期绑定连接
在请求开始时从连接池获取专属连接,整个请求的所有数据库操作都使用该连接,请求结束后释放回池。这种方式能保证同一请求内的所有操作都基于同一个已设置好search_path的连接。
以Express框架为例,用中间件实现:
const { escapeIdentifier } = require('pg'); const pool = require('./your-pool-config'); // 中间件:为请求绑定专属连接并设置schema app.use(async (req, res, next) => { try { // 从用户ID获取对应的租户schema const tenantSchema = getTenantSchemaByUserId(req.user.id); // 获取连接 req.dbConn = await pool.connect(); // 安全设置search_path(避免SQL注入) await req.dbConn.query(`SET search_path TO ${escapeIdentifier(tenantSchema)}`); next(); } catch (err) { // 出错时释放连接 if (req.dbConn) req.dbConn.release(); next(err); } }); // 路由中直接使用绑定的连接 app.get('/api/user/data', async (req, res, next) => { try { const result = await req.dbConn.query('SELECT * FROM user_profile'); res.json(result.rows); } catch (err) { next(err); } }); // 中间件:请求结束后释放连接 app.use((req, res, next) => { if (req.dbConn) req.dbConn.release(); next(); });
2. 每次获取连接时强制设置search_path
如果不想绑定请求与连接,可以在每次从池里获取连接后,立即设置对应租户的search_path,再执行数据库操作。这种方式无需绑定请求生命周期,但每次操作都要额外执行一次SET语句。
封装工具函数示例:
const { escapeIdentifier } = require('pg'); const pool = require('./your-pool-config'); async function getTenantConnection(tenantSchema) { const conn = await pool.connect(); try { // 安全设置当前连接的search_path await conn.query(`SET search_path TO ${escapeIdentifier(tenantSchema)}`); return conn; } catch (err) { conn.release(); throw err; } } // 使用示例 async function fetchTenantData(userId) { const tenantSchema = getTenantSchemaByUserId(userId); const conn = await getTenantConnection(tenantSchema); try { const result = await conn.query('SELECT * FROM order_records'); return result.rows; } finally { // 无论成功失败都释放连接 conn.release(); } }
关键注意事项
- 防止SQL注入:绝对不能直接拼接租户schema名称到
SET语句中,必须使用pg库提供的escapeIdentifier函数转义特殊字符,避免注入风险。 - 连接释放:无论使用哪种方案,都要确保连接最终被释放回池,避免连接泄漏导致池耗尽。
内容的提问来源于stack exchange,提问作者Jayadevan
相关产品推荐
相关产品推荐

