mcpField
M
multi-tenant-saas-architecture

multi-tenant-saas-architecture

github.com/peterbamuhigire开发29138 次安装4 天前更新
代码仓库

关于 multi-tenant-saas-architecture

Multi-Tenant SaaS Architecture 是用于设计和审查 SaaS 租户边界、权限、面板、会话及审计记录的 Agent 技能。它产出架构方案与基于证据的指导,重点是租户隔离和明确授权。

主要功能

  1. 01梳理租户隔离与数据范围
  2. 02定义平台、租户和终端用户面板
  3. 03设计权限优先级与拒绝规则
  4. 04规划会话、JWT 和租户身份
  5. 05制定审计事件与迁移计划

适用场景

  1. 01为新 SaaS 设计多租户架构
  2. 02将单租户应用改为多租户
  3. 03审查跨租户访问风险
  4. 04规划角色覆盖和特权操作审计

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/peterbamuhigire/chwezi-dev-engine --skill multi-tenant-saas-architecture

安装后在 Claude Code 中输入/multi-tenant-saas-architecture确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

documentation/
migration.md
references/
database-schema.md
permission-model.md
saas-deployment-models-decision-tree.md
tenant-context-propagation.md

详细介绍

多租户 SaaS 架构

致谢:由 Peter Bamuhigire、techguypeter.com、+256 784 464178 分享。

必需输入

输入必需用途
租户、产品和生命周期范围是界定 SaaS 决策
当前架构、计划、策略和约束是保留可强制执行的行为
生产数据或经验证的证据视情况而定验证阈值和迁移

能力与权限约定

默认仅进行只读分析。只有获得明确授权、遵循最小权限原则的凭据、明确租户范围、回滚方案和可审计的审批后,才能更改配置、账单、身份、租户数据、基础设施或客户沟通内容。绝不泄露机密信息,也不跨越租户边界。

降级模式

如果无法获取生产访问权限、策略、遥测数据或权威记录,则提供明确标注的设计方案或演练计划。不得声称已完成部署、协调、删除或交付,也不得声称取得了经测量的结果;应列出缺失的证据和验证项。

决策规则

条件操作停止条件
影响租户隔离、资金、身份或删除要求提供审批和回滚证据范围或权限含糊不清
证据支持可逆更改分阶段实施、测试并记录验收检查未通过
仅有部分上下文返回假设和验证项无法验证生产环境相关声明

领域反模式

  • 将一个租户的策略或数据应用于另一个租户。修正方法:在每个边界强制实施租户范围限制。
  • 根据建议请求修改生产环境。修正方法:在权限明确之前保持只读。
  • 臆造限制、价格、指标或合规声明。修正方法:使用权威记录,或将其标记为待解决。
  • 未提供回滚和审计证据就发布。修正方法:分阶段实施并保留变更前后的证明。
  • 将缺失依赖视为成功。修正方法:明确指出受阻的验证项。

适用场景

  • 为新的 SaaS 设计租户架构,或强化现有 SaaS,以防止跨租户数据泄露。
  • 将 SaaS 拆分为三个面板(超级管理员、租户/加盟商管理员、终端用户),并为其划分不同的范围。
  • 设计每个租户的角色覆盖、权限优先级顺序,以及特权访问的审计跟踪。
  • 规划会话、JWT 和 tenant_id 的接入,使下游数据库、API、安全和交付技能能够依赖这些配置。

不适用场景

  • 任务针对的是没有租户规划的单租户应用——此技能的约束只会增加开销,毫无益处。
  • 工作涉及业务模块的组合,而非租户边界——请使用 modular-saas-architecture。
  • 工作仅涉及在已定义的租户模型中调整架构——请使用 database-design-engineering 和 mysql-best-practices。

必需输入

  • 来自 system-architecture-design 的上下文映射和关键流程表。
  • 来自 database-design-engineering 的访问模式列表(以便隔离模型符合实际查询)。
  • 来自 vibe-security-skill 的威胁模型/滥用场景。
  • 来自产品需求的面板和用户类型列表(超级管理员、租户管理员、终端用户及其子类型)。

工作流

  • 按“输出”中列出的交付物,生成租户隔离映射、面板布局、权限模型和审计计划。

输出

  • 租户隔离映射、面板定义、权限优先级模型和审计计划(见下方“输出”表)。

生成的证据

类别工件格式示例
数据安全租户隔离说明Markdown 文档,涵盖行级与架构级隔离、RLS 策略以及租户范围索引docs/saas/tenant-isolation-note.md
安全租户授权审计Markdown 文档,涵盖超级管理员、租户管理员与用户之间的权限边界,以及授权测试docs/saas/tenant-authz-audit.md

参考资料

  • 阅读下面的核心工作流后,使用 references/ 目录了解详细信息。
  • 使用 documentation/ 目录获取实现细节或迁移说明。

适用于生产环境的多租户 SaaS 架构,采用三面板隔离、零信任强制机制、严格的租户隔离和全面的审计跟踪。此技能定义租户和身份验证边界,所有下游模块、API 和数据查询都会继承这些边界。

前置条件

请先按以下顺序加载:

  1. world-class-engineering — 定义整个代码库的质量标准。
  2. system-architecture-design — 生成此技能所需的上下文映射和关键流程。
  3. database-design-engineering — 生成用于制定隔离模型的访问模式列表。
  4. vibe-security-skill — 生成此技能所编码的威胁模型和身份验证规则。

适用场景

  • 从头开始为新的 SaaS 设计多租户架构。
  • 将单租户应用转换为多租户应用(参见 documentation/migration.md)。
  • 审计现有 SaaS 是否存在跨租户数据泄露、缺少 tenant_id 过滤条件或未经审计的管理员操作。
  • 规划三面板的文件和路由布局(/、/adminpanel/、/memberpanel/)。
  • 设计权限优先级阶梯(用户拒绝 > 用户授权 > 租户覆盖 > 角色 > 默认拒绝)。
  • 定义必须审计的操作以及审计记录的保留期限。

输入

工件由谁生成必需?原因
上下文映射system-architecture-design必需定义服务、面板和所有权
关键流程表system-architecture-design必需决定身份验证和会话边界
访问模式列表database-design-engineering必需选择共享数据库加 tenant_id,还是每个租户独立数据库
威胁模型vibe-security-skill必需为审计规则和滥用场景提供依据
面板和用户类型列表产品需求必需确定各面板的角色和范围

输出

工件使用方模板
租户隔离映射(隔离模型及各表的 tenant_id 规则)database-design-engineering、api-design-first内嵌(此技能)
面板定义(超级管理员、租户管理员、最终用户)api-design-first、kubernetes-saas-delivery内嵌
权限优先级模型(用户/租户覆盖/角色/默认拒绝)vibe-security-skill、api-design-first内嵌 + references/permission-model.md
会话和 JWT 方案(前缀、生命周期、轮换)api-design-first、vibe-security-skill内嵌
审计事件集和保留策略observability-monitoring、reliability-engineering内嵌
迁移方案(从单租户迁移到多租户)database-design-engineering、deployment-release-engineeringdocumentation/migration.md

不可妥协的要求

  • 每个以加盟商/租户为范围的表都必须包含 tenant_id(或 franchise_id),其值不得为 NULL,并且必须设置指向 tenants 的外键。
  • 对以租户为范围的表执行的每个查询,都必须在 WHERE 子句中包含 tenant_id。
  • tenant_id 始终从会话或 JWT 中提取,绝不能来自客户端输入(请求正文、查询参数、标头)。
  • 默认授权决策为拒绝。每项操作都必须经过明确的权限检查。
  • 每项涉及租户数据的超级管理员操作都必须记录审计,包含操作人、目标租户和操作理由。
  • 本不应存在的跨租户访问必须返回 404 Not Found,绝不能返回 403 Forbidden(403 会确认资源存在)。

决策规则

隔离模型
Tenants share data schema AND tenants < ~10k AND regulated data not tenant-partitioned
    -> Shared DB, row-level tenant_id (default choice for this skill)

Strong regulatory boundary (HIPAA/PCI) per tenant
    -> Schema-per-tenant OR DB-per-tenant; app injects connection by tenant

Single very large tenant needs isolation from the rest
    -> Hybrid: shared DB for small tenants, dedicated DB for large tenant

Wrong choice failure modes:
- Shared DB without tenant_id discipline -> data leakage, impossible to audit
- DB-per-tenant at small scale -> migration and operational cost explodes
- Schema-per-tenant without tooling -> migrations drift across schemas
新功能的面板归属
Feature controls platform config, billing, tenant lifecycle, or impersonation
    -> /adminpanel/ (super_admin only, audited)

Feature is tenant-admin workspace (manage own tenant, staff, catalogue)
    -> /public/ root (tenant owners/staff, scoped by tenant_id)

Feature is self-service for end user (customer/member/student/patient)
    -> /memberpanel/ (own records only, scoped by tenant_id AND user_id)

Wrong choice failure modes:
- Admin feature in /public/ -> tenant users see platform levers
- Tenant feature in /memberpanel/ -> end users escalate to admin-only data
- End-user feature in /public/ -> collides with tenant admin routes
权限解析优先级
1. user_permissions.denied match   -> DENY for tenant-scoped actions
2. actor is super_admin            -> ALLOW only for an explicitly scoped administrative permission (always audit)
3. user_permissions.granted match  -> ALLOW
4. tenant_role_overrides match     -> ALLOW/DENY per is_enabled
5. role -> permission via template -> ALLOW
6. no match                        -> DENY

Wrong ordering failure mode: if role is checked before user deny, an
explicit revocation never takes effect. Always deny-before-grant at each tier.
使用会话前缀还是不使用前缀
One SaaS codebase hosts multiple apps on the same origin  -> prefix required
Only one app on origin AND no shared subdomain sessions   -> prefix optional
SSO / impersonation across apps planned                   -> prefix required

Without a prefix, two SaaS apps on the same host collide in $_SESSION and the
bug only appears in production when both apps are logged in.

三面板架构

+-------------------------------------------------------------+
|                Shared Infrastructure Layer                  |
|  Data (tenant isolated) | Business Logic | Session system   |
+-------------------------------------------------------------+
         |                       |                    |
+--------v--------+    +---------v------+   +---------v-------+
|   /public/      |    |  /adminpanel/  |   |  /memberpanel/  |
| (ROOT)          |    |                |   |                 |
| Tenant Admin    |    |  Super Admin   |   |  End User       |
|  Workspace      |    |  System        |   |  Portal         |
| owner, staff    |    |  super_admin   |   |  member/student |
+-----------------+    +----------------+   +-----------------+

重要:/public/ 根目录是租户管理员工作区,而非最终用户面板。混淆两者是最常见的错误。

文件布局:

public/
  index.php           # Landing
  sign-in.php         # Login
  dashboard.php       # Tenant admin dashboard
  adminpanel/         # Super admin panel
    includes/
  memberpanel/        # End user portal
    includes/
  includes/           # Shared includes
  assets/
面板定义
面板路径用户范围备注
租户管理员/public/owner, staff单个租户所有查询都包含 tenant_id = ?;无法触及平台配置
超级管理员/public/adminpanel/super_admin跨租户tenant_id 可以为 NULL;每项操作都会记录审计日志
最终用户/public/memberpanel/member, student, customer, patient租户内的个人记录查询范围限定为 tenant_id AND user_id

租户隔离模型

各用户类型的 tenant_id 规则
用户类型tenant_id 列
super_admin可为空
租户所有者必填,NOT NULL
租户员工必填,NOT NULL
member / student / customer / patient必填,NOT NULL
表结构模式
CREATE TABLE students (
    id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id     BIGINT UNSIGNED NOT NULL,
    first_name    VARCHAR(100) NOT NULL,
    last_name     VARCHAR(100) NOT NULL,
    email         VARCHAR(100),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
    INDEX idx_tenant (tenant_id),
    INDEX idx_tenant_email (tenant_id, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

复合索引以 tenant_id 开头。所有位置都使用 utf8mb4_unicode_ci。文件名大小写必须完全一致(Linux 文件系统区分大小写)。

查询强制执行
// Extract tenant from session/JWT. Never from client input.
$tenantId = getSession('tenant_id');

// Regular users: filter by tenant_id
$stmt = $db->prepare('SELECT * FROM students WHERE tenant_id = ? AND id = ?');
$stmt->execute([$tenantId, $studentId]);

// Super admin: cross-tenant access is allowed but always audited
if (getSession('user_type') === 'super_admin') {
    auditLog('CROSS_TENANT_ACCESS', [
        'admin_user_id'      => getSession('user_id'),
        'target_tenant_id'   => $requestedTenantId,
        'action'             => 'VIEW_STUDENTS',
        'justification'      => $request['justification'] ?? null,
    ]);
}

Session 和 JWT 方案

Session 前缀系统
define('SESSION_PREFIX', 'saas_app_');  // e.g. 'school_', 'restaurant_'

setSession('user_id', $userId);      // writes $_SESSION['saas_app_user_id']
$userId = getSession('user_id');     // reads $_SESSION['saas_app_user_id']
Session Cookie 设置
设置值
HttpOnlytrue
Secure自动检测 HTTPS(端口 443 或 $_SERVER['HTTPS'])
SameSiteStrict
有效期空闲 30 min
重新生成登录时和权限变更时
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
    || (int) $_SERVER['SERVER_PORT'] === 443;
ini_set('session.cookie_secure', $isHttps ? '1' : '0');
用于移动端和 API 的 JWT
设置值
访问令牌有效期15 min
刷新令牌有效期30 days
轮换每次刷新时
撤销存储在以 jti 为键的表中

权限模型

优先级(从高到低):用户拒绝 > 用户授权 > 租户覆盖 > 角色权限 > 默认拒绝。完整的架构、解析算法、缓存策略和种子数据请参阅 references/permission-model.md。

function hasPermission(userId, tenantId, permission) {
  if (userPermissions.denied(userId, tenantId, permission)) return false;
  if (user.type === 'super_admin' && isScopedAdministrativePermission(permission)) {
    auditLog('PERMISSION_BYPASS', { userId, permission });
    return true;
  }
  if (userPermissions.granted(userId, tenantId, permission)) return true;
  for (const role of getUserRoles(userId, tenantId)) {
    const override = tenantRoleOverride(tenantId, role.id, permission);
    if (override !== null) return override.isEnabled;
    if (roleHasPermission(role, permission)) return true;
  }
  return false; // default deny
}

零信任检查清单

  • 对租户范围表的每个查询都包含 tenant_id(带审计的 super_admin 除外)。
  • 仅使用预处理语句 — 不得将字符串拼接到 SQL 中。
  • 每次执行变更操作前都要检查权限。
  • 超级管理员访问必须启用 MFA。
  • 密码以 Argon2ID 存储,并使用每用户盐值(32 chars)和全服务器 pepper(64+ chars)。
  • 连续失败 5 次后锁定账户,并采用指数退避。
  • 速率限制:每个租户 1000 req/min,每个用户 100 req/min。
  • 所有面板均启用 HTTPS + HSTS。
  • 超级管理员操作必须附带理由并记录审计日志。
  • 跨租户访问返回 404,而不是 403。

API 设计

# Tenant-scoped (default)
GET    /api/v1/orders          # list within tenant
POST   /api/v1/orders          # create within tenant
GET    /api/v1/orders/{id}     # show; 404 if wrong tenant
DELETE /api/v1/orders/{id}     # delete; 404 if wrong tenant

# Super admin (cross-tenant, audited)
GET    /api/v1/admin/tenants
POST   /api/v1/admin/impersonate

响应封装:

{ "success": true, "data": {}, "meta": { "page": 1, "total": 100 } }

错误封装:

{ "success": false, "error": { "code": "PERMISSION_DENIED", "message": "..." } }

审计与合规

始终记录审计日志:所有超级管理员操作、身份模拟、权限变更、租户创建/暂停、数据导出、身份验证失败、跨租户访问尝试。

{
  "id": "uuid",
  "timestamp": "2026-04-07T10:30:00Z",
  "actor_user_id": 123,
  "actor_type": "super_admin",
  "action": "IMPERSONATE_USER",
  "target_tenant_id": 456,
  "justification": "Support request #12345",
  "ip_address": "203.0.113.1",
  "changes": { "before": {}, "after": {} }
}

保留期限:安全日志 1 year,审计记录 7 years,运维日志 90 days。

监控告警

  • 跨租户访问尝试(目标 = 零)。
  • 超级管理员从新 IP 登录。
  • 身份验证失败激增 > 100/min。
  • 对租户范围表的数据库查询未包含 tenant_id。
  • API 错误率 > 5%。

租户生命周期

PENDING -> ACTIVE -> SUSPENDED -> ARCHIVED

在达到配置的保留期限截止点之前,归档操作可撤销;之后将通过级联方式彻底删除已归档的租户。

反模式

  • 由客户端提供租户标识符。 $franchiseId = $_POST['franchise_id']; — 任何用户都能将其设为其他租户的标识符。修复:始终使用 getSession('tenant_id'),或读取已验证 JWT 中的声明。
  • 查询缺少租户范围。 SELECT * FROM students WHERE id = ? — 会返回任意租户的学生。修复:使用 SELECT * FROM students WHERE tenant_id = ? AND id = ?,并将未找到记录的情况视为 404。
  • 超级管理员执行变更但未审计。 在超级管理员面板中运行 deleteStudent($studentId);,但没有记录。修复:使用 auditLog('ADMIN_DELETE_STUDENT', [...]) 包装每项超级管理员变更,记录操作者、目标租户和理由。
  • 跨租户访问返回 403。 这会确认资源存在,并允许枚举租户 ID。修复:将跨租户未命中映射为 404,隐藏资源是否存在。
  • 业务表中的 tenant_id 允许为空。 tenant_id BIGINT NULL — 允许存在没有所有者的行,随后每个租户查询都会静默排除这些行。修复:将其设为 tenant_id BIGINT UNSIGNED NOT NULL 并添加外键;在切换为 NOT NULL 前先回填数据。
  • 多应用主机上的会话键没有前缀。 同一源上的两个应用会覆盖彼此的 $_SESSION['user_id']。修复:为每个应用设置 SESSION_PREFIX,并始终通过 setSession() / getSession() 操作会话。
  • 索引顺序使用 (status, tenant_id) 而非 (tenant_id, status)。 查询规划器无法使用复合索引筛选出单个租户的数据。修复:每个复合索引都以 tenant_id 开头。
  • 一次性迁移所有表。 这会增加回滚风险,并造成数小时的停机。修复:按照 documentation/migration.md 中的六步模式,每次迁移一个领域,并在将列切换为 NOT NULL 前确认 tenant_id 没有 NULL 值。

接下来阅读

  • database-design-engineering — 确定隔离模型后,了解架构、索引和迁移规范。
  • api-design-first — REST 约定、身份验证模型和错误封装;本技能中的面板依赖这些内容。
  • vibe-security-skill — 威胁建模、滥用场景以及为本技能提供依据的身份验证 / 授权矩阵。
  • kubernetes-saas-delivery — 在此模型基础上实现按租户部署、命名空间隔离和渐进式发布。
  • modular-saas-architecture — 当平台还要在此处定义的租户体系内组合可插拔业务模块时阅读。
  • mysql-best-practices — 了解如何针对 MySQL 引擎实现此处的架构模式。

参考资料

  • references/database-schema.md — 租户、用户、审计和租户范围表的架构、索引与分区。
  • references/permission-model.md — RBAC 架构、权限解析算法、缓存、中间件、层级权限和条件权限。
  • documentation/migration.md — 安全添加 tenant_id、零停机迁移、从单租户到多租户的阶段以及回滚。

AI 服务隔离补充说明

当涉及 AI 时,租户隔离模式还要深入一层。本技能中的事务模式(RLS / 每租户一个架构 / 每租户一个数据库)还适用于 AI 资产类别 — 向量存储、提示词、微调模型、评估数据集、对话日志、检索缓存和审计日志负载。专门的技能是 ai-tenant-isolation-patterns。

交叉参考:

  • ai-on-saas-architecture — 统一的 AI+SaaS 架构。
  • ai-tenant-isolation-patterns — 向量存储分区、纵深防御、BYOK、数据泄漏测试套件。
  • ai-rag-multi-tenant — RAG 专属隔离。
  • ai-model-gateway — 在请求边界强制执行租户范围。
  • ai-prompt-injection-and-tenant-safety — 提示词层面的对抗性补充。

评论

支持 Markdown