项目概述
本项目实现行级隔离的多租户 API:从令牌解析租户,在连接上设置会话变量,并依赖行级安全策略按该变量过滤所有表,使应用代码无需手写租户过滤。隔离若写在应用层,第一次漏掉过滤就是跨租户泄露,因此把它下沉到数据库。
架构与关键决策
请求从令牌解析租户后设置连接会话变量;每张表上的策略引用该变量,使查询无论怎么写都只返回当前租户的行;变量未设置时策略不匹配任何行,因此跳过租户解析只会得到空结果而非全表。连接包装器在借用时设置、归还时清空,使池化连接不会把某租户的上下文带入另一请求;配额按租户限制共享容量。
实施与测试
建立两个租户,以 A 写入并断言 B 查询为空;模拟缺失会话变量断言零行而非全部;在小连接池上并发不同租户断言互不可见;断言写入也不能归属到其他租户。
限制与后续
- 共享 schema 下的行级隔离,不提供按租户独立库。
- 无按租户的 schema 定制。
- 配额为请求与连接的基础限制,非完整计费模型。
代码示例
pnpm init
pnpm add pg express
pnpm add -D typescript tsx vitest
multi-tenant-api/
├── src/tenant.ts
├── src/db.ts
├── src/routes.ts
├── db/policies.sql
├── tests/isolation.test.ts
└── package.json
const tenantId = claims.tenant; if (!tenantId) return res.status(403).end();
await client.query('SET LOCAL app.tenant_id = $1', [tenantId]);
ALTER TABLE records ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON records USING (tenant_id = current_setting('app.tenant_id'));
await client.query('RESET app.tenant_id');
if (await overQuota(tenantId)) return res.status(429).end();
pnpm vitest run