关于 multitenancy
此技能介绍多租户应用架构,涵盖租户隔离方案和请求级租户识别。内容包括实现模式、反模式及供开发者使用的生产检查清单。
主要功能
- 01比较数据库、Schema 与行级隔离
- 02展示共享 Schema 的租户范围模式
- 03介绍 PostgreSQL 行级安全
- 04列出租户识别策略
- 05提供生产环境隔离检查清单
适用场景
- 01为 SaaS 选择隔离策略
- 02为 Prisma 查询限定租户范围
- 03应用 PostgreSQL 行级安全
- 04上线前审查租户隔离
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill multitenancy安装后在 Claude Code 中输入/multitenancy确认已加载。
详细介绍
多租户架构
隔离策略
| 策略 | 隔离级别 | 复杂度 | 成本 |
|---|---|---|---|
| 每个租户一个数据库 | 最高 | 高 | 高 |
| 每个租户一个架构 | 高 | 中 | 中 |
| 共享架构(tenant_id 列) | 中 | 低 | 低 |
| 行级安全性(RLS) | 中高 | 中 | 低 |
使用租户 ID 的共享架构(最常见)
// Middleware: resolve tenant from subdomain or header
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const host = req.hostname; // acme.myapp.com
const subdomain = host.split('.')[0];
const tenant = await tenantRepo.findBySubdomain(subdomain);
if (!tenant) return res.status(404).json({ error: 'Tenant not found' });
req.tenantId = tenant.id;
next();
}
// Always filter by tenant
app.get('/api/products', async (req, res) => {
const products = await db.product.findMany({
where: { tenantId: req.tenantId },
});
res.json(products);
});
使用 Prisma 进行租户范围限定
// Extension to auto-apply tenant filter
const prisma = new PrismaClient().$extends({
query: {
$allOperations({ args, query, operation }) {
if (['findMany', 'findFirst', 'count', 'updateMany', 'deleteMany'].includes(operation)) {
args.where = { ...args.where, tenantId: getCurrentTenantId() };
}
if (['create', 'createMany'].includes(operation)) {
args.data = { ...args.data, tenantId: getCurrentTenantId() };
}
return query(args);
},
},
});
PostgreSQL 行级安全性
-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
-- Policy: users see only their tenant's data
CREATE POLICY tenant_isolation ON products
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Set tenant context per request
SET app.tenant_id = 'tenant-uuid-here';
SELECT * FROM products; -- auto-filtered
// Set tenant context on each request
pool.on('connect', async (client) => {
// Set after getting connection from pool
});
async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query(`SET app.tenant_id = $1`, [tenantId]);
return await fn();
} finally {
await client.query('RESET app.tenant_id');
client.release();
}
}
每个租户一个架构
// Dynamic schema selection
function getTenantSchema(tenantId: string): string {
return `tenant_${tenantId.replace(/-/g, '_')}`;
}
async function createTenantSchema(tenantId: string) {
const schema = getTenantSchema(tenantId);
await db.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
await db.query(`SET search_path TO ${schema}`);
await runMigrations(); // Apply schema migrations
}
租户解析策略
| 策略 | 示例 | 最适用场景 |
|---|---|---|
| 子域名 | acme.myapp.com | B2B SaaS |
| 路径前缀 | myapp.com/acme/... | 简单配置 |
| 自定义请求头 | X-Tenant-ID: acme | API 优先 |
| JWT 声明 | { tenantId: "acme" } | 经过身份验证的 API |
反模式
| 反模式 | 解决方法 |
|---|---|
| 查询中未进行租户筛选 | 使用中间件或 ORM 扩展自动应用筛选 |
| 使用未经验证的客户端提供的租户 ID | 从身份验证令牌或子域名中获取 |
| 未测试租户数据隔离 | 编写测试以验证租户间隔离 |
| 共享缓存键未添加租户前缀 | 为所有缓存键添加租户 ID 前缀 |
| 未按租户进行速率限制 | 按租户进行速率限制,而非全局限制 |
生产环境检查清单
- 在所有路由上配置租户解析中间件
- 通过自动化测试验证数据隔离
- 为缓存键添加租户 ID 前缀
- 按租户进行速率限制
- 使用租户范围限定的后台任务
- 租户配置和停用流程
- 防止跨租户查询(通过 RLS 或 ORM 强制执行)
评论
支持 Markdown