Hotdry.

Article

Medusa 模块化商业平台的多租户数据隔离设计:基于 PostgreSQL RLS 的工程实践

深入解析 Medusa 2.0 模块化架构下的多租户数据隔离方案,提供基于 PostgreSQL RLS 的可落地实现路径与生产环境检查清单。

2026-05-17web

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 数据库。

这种架构为多租户提供了天然的扩展点:可以在模块层之上构建租户上下文,而无需修改核心商业逻辑。

多租户数据隔离的核心挑战

传统的应用层隔离存在明显缺陷:

  1. 容易遗漏:开发人员可能在某些查询中忘记添加租户过滤条件
  2. SQL 注入风险:恶意构造的请求可能绕过应用层的过滤逻辑
  3. 维护成本高:每个查询都需要显式处理租户条件

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.acquireConnectionclient.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%,但需要遵循以下最佳实践:

  1. 索引优化:为所有表的 tenant_id 列创建索引
  2. 连接池优化:使用 WeakMap 跟踪每个连接的租户上下文,避免重复执行 set_config
  3. 监控查询计划:定期检查执行计划,确保 RLS 策略正确使用索引

局限与应对

Framework Patch 维护成本:每次升级 @medusajs/framework 版本后,需要重新验证 Patch 的兼容性。建议建立升级测试流程,在升级后立即运行多租户集成测试套件。

跨租户查询限制:RLS 启用后,应用层无法直接执行跨租户查询。如需实现 Admin 仪表盘的全局视图,需通过不设置租户上下文的方式进入 Admin 模式,或在查询中显式使用 SET ROLE 切换。


资料来源

web

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com