Medusa 2.0 作为以模块化架构为核心的 Headless 商业平台,在支持多租户(Multi-Tenant)场景时面临一个关键设计决策:如何在保持模块松耦合的同时,实现严格的数据隔离。与在应用层手动添加 WHERE tenant_id = ? 的方式相比,PostgreSQL 的行级安全(Row Level Security, RLS)提供了一种更彻底、更安全的隔离机制。
Medusa 2.0 的模块化架构
Medusa 2.0 将业务领域封装为独立的模块(Modules),如 Product、Order、Customer 等,每个模块包含自己的数据模型、服务和 API。第三方集成则通过 ** 插件(Plugins)** 实现,这种分离使得核心商业逻辑与外部服务解耦。根据官方文档,请求流经四个层次:API Routes → Workflows → Modules → PostgreSQL 数据库。
这种架构为多租户提供了天然的扩展点:可以在模块层之上构建租户上下文,而无需修改核心商业逻辑。
多租户数据隔离的核心挑战
传统的应用层隔离存在明显缺陷:
- 容易遗漏:开发人员可能在某些查询中忘记添加租户过滤条件
- SQL 注入风险:恶意构造的请求可能绕过应用层的过滤逻辑
- 维护成本高:每个查询都需要显式处理租户条件
PostgreSQL RLS 通过在数据库层面强制执行访问策略,从根本上解决了这些问题。一旦配置完成,所有查询都会自动根据当前会话的租户上下文进行过滤,应用层无需任何改动。
RLS 三层实现架构
第一层:HTTP Middleware 提取租户标识
在 src/modules/tenant-context/middleware.ts 中创建中间件,从请求头(如 x-tenant-id)或 JWT Token 中提取租户 ID,并通过 Node.js 的 AsyncLocalStorage 存储,确保该上下文在整个异步请求生命周期内可用。
export const tenantContextStorage = new AsyncLocalStorage<TenantContext>();
export function tenantContextMiddleware(req, res, next) {
const tenantId = req.headers['x-tenant-id'];
if (tenantId) {
return tenantContextStorage.run({ tenantId }, () => next());
}
next(); // 无租户标识时进入 Admin 模式
}
第二层:Framework Patch 注入会话变量
这是实现 RLS 的关键步骤。通过 patch-package 修改 @medusajs/framework 的数据库连接加载器,在获取数据库连接时,从 AsyncLocalStorage 读取租户 ID,并设置为 PostgreSQL 会话变量:
SELECT set_config('app.current_tenant', 'tenant-uuid', false);
该 Patch 会拦截 client.acquireConnection 和 client.query,确保每次查询前都设置正确的租户上下文。
第三层:Database Policies 自动过滤
通过迁移文件为所有业务表启用 RLS 并创建策略:
-- 添加 tenant_id 列
ALTER TABLE product ADD COLUMN tenant_id UUID;
CREATE INDEX idx_product_tenant_id ON product(tenant_id);
-- 启用 RLS
ALTER TABLE product ENABLE ROW LEVEL SECURITY;
ALTER TABLE product FORCE ROW LEVEL SECURITY;
-- 创建策略:只允许访问匹配当前租户的数据
CREATE POLICY tenant_isolation_policy ON product
FOR ALL
USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
策略的核心逻辑是:当 app.current_tenant 会话变量被设置时,只返回该租户的数据;当变量为空时(Admin 模式),返回所有数据。
可落地的参数与配置
数据库角色配置
关键前提:必须使用非 superuser 角色连接数据库,否则 RLS 会被 PostgreSQL 自动绕过。
-- 创建非 superuser 应用账号
CREATE USER medusa_app_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE medusa_db TO medusa_app_user;
GRANT ALL ON SCHEMA public TO medusa_app_user;
GRANT ALL ON ALL TABLES IN SCHEMA public TO medusa_app_user;
环境变量配置
# 应用运行时使用非 superuser
DATABASE_URL=postgresql://medusa_app_user:secure_password@localhost:5432/medusa_db
# 迁移时使用 superuser(单独保存)
DATABASE_SUPER_URL=postgresql://postgres:password@localhost:5432/medusa_db
需要启用 RLS 的核心表清单
Medusa 2.0 中建议对以下 44+ 张表启用 RLS:
- 商品相关:product, product_variant, product_option, product_collection, product_category
- 订单相关:order, order_item, cart, line_item, return, return_reason
- 客户相关:customer, customer_group, customer_address
- 支付相关:payment_collection, payment, refund
- 库存相关:inventory_item, inventory_level, reservation_item
- 销售相关:sales_channel, region, store, currency
- 营销相关:promotion, campaign, discount, price_list
生产环境检查清单
在部署到生产环境前,请逐项验证:
数据库配置
- 非 superuser 角色已创建并配置
- 应用使用非 superuser 凭据连接
- RLS 迁移已成功执行
- 所有业务表已启用 RLS(
rls_enabled = t) - 每张表已创建 4 个策略(SELECT/INSERT/UPDATE/DELETE)
应用配置
- Framework Patch 已成功应用
-
postinstall脚本已配置patch-package - 租户上下文模块已注册到
medusa-config.ts - 中间件已全局注册到
src/api/middlewares.ts - 身份验证中间件在租户上下文中间件之前执行
验证命令
# 验证 Patch 是否生效
grep -c "RLS_PATCH" node_modules/@medusajs/framework/dist/database/pg-connection-loader.js
# 验证当前用户是否为非 superuser
psql $DATABASE_URL -c "SELECT current_user, usesuper FROM pg_user WHERE usename = current_user;"
# 预期输出:usesuper = f
# 验证 RLS 状态
psql $DATABASE_URL -c "SELECT COUNT(*) FROM check_rls_status();"
# 预期输出:44(表数量)
性能考量与优化
RLS 对查询性能的影响通常小于 5%,但需要遵循以下最佳实践:
- 索引优化:为所有表的
tenant_id列创建索引 - 连接池优化:使用
WeakMap跟踪每个连接的租户上下文,避免重复执行set_config - 监控查询计划:定期检查执行计划,确保 RLS 策略正确使用索引
局限与应对
Framework Patch 维护成本:每次升级 @medusajs/framework 版本后,需要重新验证 Patch 的兼容性。建议建立升级测试流程,在升级后立即运行多租户集成测试套件。
跨租户查询限制:RLS 启用后,应用层无法直接执行跨租户查询。如需实现 Admin 仪表盘的全局视图,需通过不设置租户上下文的方式进入 Admin 模式,或在查询中显式使用 SET ROLE 切换。
资料来源
- Medusa 官方文档:Architecture Overview
- Rigby 技术指南:Implement Multi-Tenancy in Medusa with PostgreSQL RLS
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。