关于 multi-tenant-saas-architecture
Multi-Tenant SaaS Architecture 是用于设计和审查 SaaS 租户边界、权限、面板、会话及审计记录的 Agent 技能。它产出架构方案与基于证据的指导,重点是租户隔离和明确授权。
主要功能
- 01梳理租户隔离与数据范围
- 02定义平台、租户和终端用户面板
- 03设计权限优先级与拒绝规则
- 04规划会话、JWT 和租户身份
- 05制定审计事件与迁移计划
适用场景
- 01为新 SaaS 设计多租户架构
- 02将单租户应用改为多租户
- 03审查跨租户访问风险
- 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.mdreferences/database-schema.mdpermission-model.mdsaas-deployment-models-decision-tree.mdtenant-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 和数据查询都会继承这些边界。
前置条件
请先按以下顺序加载:
world-class-engineering— 定义整个代码库的质量标准。system-architecture-design— 生成此技能所需的上下文映射和关键流程。database-design-engineering— 生成用于制定隔离模型的访问模式列表。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-engineering | documentation/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 设置
| 设置 | 值 |
|---|---|
| HttpOnly | true |
| Secure | 自动检测 HTTPS(端口 443 或 $_SERVER['HTTPS']) |
| SameSite | Strict |
| 有效期 | 空闲 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— 提示词层面的对抗性补充。