當您在建置多租戶 SaaS 時,第一個架構問題通常是:如何保持租戶資料隔離?選項從每個租戶使用獨立資料庫(最高隔離性、最高成本)到共用資料庫搭配資料列層級過濾(最低成本,需要更謹慎的編碼)。

但有一個同等重要的問題較少受到關注:您的 API 如何知道請求屬於哪個租戶上下文?

這篇文章介紹我們在生產環境中使用的模式:使用自訂請求標頭進行租戶範圍界定,並結合 JWT 驗證。實作簡單、易於稽核,且靈活支援單一使用者帳戶的多租戶存取。

三種常見方法

1. 基於子網域(tenant.yourdomain.com

租戶編碼在主機名稱中。每個子網域路由到同一個後端,後端從 Host 標頭中提取租戶。

優點:直觀、在 URL 中可見。
缺點:需要萬用字元 TLS 憑證、更複雜的 DNS 設定、在開發環境中不方便、行動 API 客戶端的運作方式不同。

2. 基於 URL 路徑(/api/tenants/{tenantId}/...

租戶識別碼是每個路由路徑的一部分。

優點:符合 REST 原則、自我說明。
缺點:使所有路由定義變得冗長、每個端點都需要包含租戶區段、使 API 版本控制變得更複雜。

3. 基於標頭(x-tenant-id: <id>

自訂標頭攜帶租戶上下文。路由保持簡潔。租戶範圍在中介軟體中解析,然後才執行處理程序。

優點:路由保持簡單、中介軟體統一處理範圍界定、與 JWT 驗證搭配良好、易於測試。
缺點:較不明顯(租戶不在 URL 中)、客戶端必須始終包含標頭。

我們使用標頭方法。

實作方式

API 接受兩種驗證形式:

  1. Authorization 標頭中的 JWT 權杖 — 識別發出請求
  2. x-tenant-id 標頭中的租戶 ID — 識別代表哪個租戶
POST /api/v1/members
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
x-tenant-id: tenant_01GZ8K3X7Y
Content-Type: application/json

Enter fullscreen mode Exit fullscreen mode

中介軟體

驗證中介軟體首先執行並驗證 JWT。租戶中介軟體其次執行,並根據已驗證使用者的允許租戶清單驗證 x-tenant-id

// middleware/requireAuth.js
export async function requireAuth(req, res, next) {
  const token = extractBearerToken(req.headers.authorization);
  if (!token) return res.status(401).json({ error: 'Unauthorized' });

  try {
    const payload = verifyJwt(token);
    req.user = payload;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

Enter fullscreen mode Exit fullscreen mode

// middleware/requireTenantContext.js
export async function requireTenantContext(req, res, next) {
  const tenantId = req.headers['x-tenant-id'];
  if (!tenantId) return res.status(400).json({ error: 'x-tenant-id header required' });

  // Verify the authenticated user has access to this tenant
  const membership = await db.membership.findFirst({
    where: {
      userId: req.user.id,
      tenantId,
      status: 'active',
    },
  });

  if (!membership) return res.status(403).json({ error: 'Access denied' });

  req.tenantId = tenantId;
  req.role = membership.role;
  next();
}

Enter fullscreen mode Exit fullscreen mode

路由處理程序接著可使用 req.tenantIdreq.role。這些處理程序中的所有資料庫查詢都包含 where: { tenantId: req.tenantId }

路由註冊

租戶範圍的路由會套用兩個中介軟體。公開路由(驗證端點、健康檢查)不套用任何中介軟體:

// Public
router.post('/auth/login', loginHandler);
router.get('/health', healthHandler);

// Tenant-scoped
router.use('/members', requireAuth, requireTenantContext, membersRouter);
router.use('/invoices', requireAuth, requireTenantContext, invoicesRouter);
router.use('/settings', requireAuth, requireTenantContext, settingsRouter);

Enter fullscreen mode Exit fullscreen mode

中介軟體在路由層級套用,而非每個處理程序。新路由在租戶範圍的前綴下會自動繼承上下文,無需任何額外工作。

單一帳戶的多租戶存取

標頭模式使另一件事變得容易:單一使用者帳戶存取多個租戶。

超級管理員或管理工具需要跨租戶查詢或切換上下文,而無需重新驗證。使用標頭模式這很簡單 — 為使用者發出一個 JWT,然後每個請求傳遞不同的 x-tenant-id 值:

// Management dashboard switching tenant context
async function fetchMembersForTenant(tenantId) {
  return api.get('/members', {
    headers: {
      'Authorization': `Bearer ${userToken}`,
      'x-tenant-id': tenantId,
    }
  });
}

Enter fullscreen mode Exit fullscreen mode

使用基於子網域或路徑的方法,同樣的情境需要不同的基礎 URL 或重複的路由結構。

加入深度防禦:資料列層級安全性

標頭 + 中介軟體模式處理應用程式層的租戶隔離。若要在資料庫層級增加一層防護,PostgreSQL 的資料列層級安全性(Row-Level Security)即使應用程式程式碼有錯誤而遺漏 tenantId 過濾器,仍能強制執行隔離:

-- Policy: users can only see rows belonging to their current tenant
ALTER TABLE members ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON members
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Enter fullscreen mode Exit fullscreen mode

在每個請求開始時,在資料庫連線上設定目前租戶:

await db.$executeRaw`SELECT set_config('app.current_tenant_id', ${req.tenantId}, true)`;

Enter fullscreen mode Exit fullscreen mode

即使查詢忘記 WHERE tenant_id = ?,現在也會傳回空結果,而不會洩漏資料。中介軟體是第一道防線;RLS 是最後一道防線。

採用前需了解的權衡

客戶端必須始終傳送標頭。這是一項紀律要求。忘記傳送標頭會傳回 400,這很容易偵錯,但這是初始整合時的小困擾。清楚記錄並考慮提供有幫助的錯誤訊息:"x-tenant-id header is required for this endpoint. See docs for details."

標頭在日誌中可見。租戶 ID 不是機密 — 它們是識別碼 — 但請確保您的日誌清理規則會將它們與其他中繼資料一致對待。

在工作階段中切換租戶是應用程式層邏輯。API 不知道也不關心工作階段狀態中的「目前租戶」。客戶端始終告訴 API 要使用哪個租戶上下文。這是明確的,這很好,但需要客戶端管理該狀態。

摘要

x-tenant-id 標頭模式雖然不華麗,但很有效。路由保持簡潔。中介軟體統一處理範圍界定。單一 JWT 可跨多個租戶上下文運作。當您需要時,此模式可自然地與資料庫層級的隔離機制搭配使用。

對於大多數租戶是組織(而非個別使用者)的多租戶 API,在使用子網域或基於路徑的替代方案之前,值得考慮此模式。