多租户
规则
- 行级隔离(默认):在每个租户范围内的表中添加
tenantId 列,并通过中间件或 RLS(行级安全)强制执行。
- 每个租户一个 Schema:当租户需要自定义 Schema 或严格数据隔离时使用。操作更复杂,但隔离性更好。
- 子域名路由:从
{tenant}.app.com 中解析租户。在中间件中解析,并将 tenantId 设置在请求上下文中。
- 自定义域名:将 CNAME 指向您的应用,通过
Host 标头查找租户。使用 domains 表将自定义域名映射到租户 ID。
- 每租户配置:将主题(Logo、颜色)、功能标志和限制存储在
tenant_settings 表中,并在内存中缓存(带 TTL)。
- RLS 模式 (Postgres):
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON posts USING (tenant_id = current_setting('app.tenant_id'))
- 中间件:从子域名/标头提取租户,设置到上下文中,并应用于所有数据库查询——绝不允许查询在没有租户范围的情况下运行。
- 数据分区:对于大型表,按
tenantId 分区——可提高查询性能,并支持按租户进行备份/恢复。
行级隔离模式
function getTenantFromHost(host: string): string {
const subdomain = host.split(".")[0];
return subdomain;
}
prisma.$use(async (params, next) => {
if (TENANT_MODELS.includes(params.model)) {
params.args.where = { ...params.args.where, tenantId: context.tenantId };
}
return next(params);
});
子域名路由 (Next.js 中间件)
export function middleware(request: NextRequest) {
const host = request.headers.get("host") ?? "";
const tenant = host.split(".")[0];
const response = NextResponse.next();
response.headers.set("x-tenant-id", tenant);
return response;
}
避免
- 没有租户范围的查询——缺少
WHERE tenantId = ? 会跨租户泄露数据
- 在客户端硬编码租户——应从服务器端子域名/域名解析
- 没有
tenantId 索引——每个租户范围内的表都需要一个以 tenantId 开头的复合索引
- 在共享缓存中混合租户数据——使用
{tenantId}:{key} 命名空间化缓存键