multi-tenant-saas-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRequired Plugins
必备插件
Superpowers plugin: MUST be active for all work using this skill. Use throughout the entire build pipeline — design decisions, code generation, debugging, quality checks, and any task where it offers enhanced capabilities. If superpowers provides a better way to accomplish something, prefer it over the default approach.
Superpowers 插件: 所有使用此技能的工作都必须激活该插件。在整个构建流程中使用——包括设计决策、代码生成、调试、质量检查以及任何它能提供增强功能的任务。如果Superpowers插件提供了更优的实现方式,优先使用它而非默认方法。
Multi-Tenant SaaS Platform Architecture
多租户SaaS平台架构
Overview
概述
Production-grade multi-tenant SaaS architecture with strict tenant isolation, zero-trust security, and three-panel separation.
Core Principles:
- Zero-trust: Every request authenticated, authorized, validated
- Tenant isolation by default: No cross-tenant data access
- Least privilege: Granular, explicit, auditable permissions
- Audit everything: Immutable audit trails for privileged operations
Security Baseline (Required): Always load and apply the Vibe Security Skill for any web app, API, or data access work. Its controls are mandatory alongside multi-tenant patterns.
Database Standards (Required): All database work (schema design, migrations, stored procedures, queries) MUST follow mysql-best-practices skill patterns. See that skill's migration checklist for required pre/post-migration steps.
See subdirectories for:
- - Database schemas (database-schema.md), permission models (permission-model.md)
references/ - - Migration patterns (migration.md)
documentation/
生产级多租户SaaS架构,具备严格租户隔离、零信任安全和三面板分离特性。
核心原则:
- 零信任:每一个请求都需经过认证、授权和验证
- 默认租户隔离:禁止跨租户数据访问
- 最小权限:细粒度、明确且可审计的权限
- 全量审计:特权操作的不可变审计追踪
安全基线(必填): 所有Web应用、API或数据访问工作都必须加载并应用Vibe Security Skill。其控制措施是多租户模式的强制性要求。
数据库标准(必填): 所有数据库工作( schema设计、迁移、存储过程、查询)都必须遵循mysql-best-practices技能模式。参考该技能的迁移清单获取必填的迁移前后步骤。
子目录说明:
- - 数据库schema(database-schema.md)、权限模型(permission-model.md)
references/ - - 迁移模式(migration.md)
documentation/
Deployment Environments
部署环境
Multi-tenant apps must work identically across all environments:
| Environment | OS | Database | Web Root |
|---|---|---|---|
| Development | Windows 11 (WAMP) | MySQL 8.4.7 | |
| Staging | Ubuntu VPS | MySQL 8.x | |
| Production | Debian VPS | MySQL 8.x | |
Cross-platform rules: Use collation everywhere. Match file/directory case exactly (Linux is case-sensitive). Production migrations must be non-destructive and idempotent ().
utf8mb4_unicode_cidatabase/migrations-production/多租户应用必须在所有环境中保持一致运行:
| 环境 | 操作系统 | 数据库 | Web根目录 |
|---|---|---|---|
| 开发环境 | Windows 11 (WAMP) | MySQL 8.4.7 | |
| 预发布环境 | Ubuntu VPS | MySQL 8.x | |
| 生产环境 | Debian VPS | MySQL 8.x | |
跨平台规则: 所有场景使用排序规则。严格匹配文件/目录大小写(Linux区分大小写)。生产环境迁移必须是非破坏性且幂等的()。
utf8mb4_unicode_cidatabase/migrations-production/When to Use
适用场景
✅ Multi-tenant SaaS platforms
✅ Strict tenant data isolation required
✅ Role-based permissions with admin oversight
✅ Compliance and audit trail requirements
✅ Multiple user types (internal staff, external customers)
❌ Single-tenant applications
❌ Simple CRUD apps without isolation needs
❌ Internal tools with flat permission models
✅ 多租户SaaS平台
✅ 需要严格租户数据隔离
✅ 带管理员监督的基于角色的权限
✅ 合规性和审计追踪要求
✅ 多种用户类型(内部员工、外部客户)
❌ 单租户应用
❌ 无需隔离的简单CRUD应用
❌ 权限模型扁平化的内部工具
Three-Tier Panel Architecture
三面板架构
THIS IS THE CORE ARCHITECTURAL CONCEPT:
┌──────────────────────────────────────────────────────────────┐
│ Shared Infrastructure Layer │
│ ┌───────────┬─────────────┬─────────────┬────────────┐ │
│ │ Data │ Business │ Integration │ Session │ │
│ │ (Tenant │ Logic │ Layer │ Prefix │ │
│ │ Isolated) │ (Scoped) │ (External) │ System │ │
│ └───────────┴─────────────┴─────────────┴────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │ │
┌────────▼────────┐ ┌───▼──────┐ ┌──────────▼──────┐
│ /public/ │ │/adminpanel│ │ /memberpanel/ │
│ (ROOT) │ │ │ │ │
│ Franchise Admin │ │Super Admin│ │ End User │
│ Workspace │ │ System │ │ Portal │
│ │ │ │ │ │
│ owner, staff │ │super_admin│ │member, student, │
│ │ │ │ │customer, patient│
└─────────────────┘ └───────────┘ └─────────────────┘CRITICAL: root is the FRANCHISE ADMIN WORKSPACE, not a member panel!
/public/这是核心架构概念:
┌──────────────────────────────────────────────────────────────┐
│ Shared Infrastructure Layer │
│ ┌───────────┬─────────────┬─────────────┬────────────┐ │
│ │ Data │ Business │ Integration │ Session │ │
│ │ (Tenant │ Logic │ Layer │ Prefix │ │
│ │ Isolated) │ (Scoped) │ (External) │ System │ │
│ └───────────┴─────────────┴─────────────┴────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │ │
┌────────▼────────┐ ┌───▼──────┐ ┌──────────▼──────┐
│ /public/ │ │/adminpanel│ │ /memberpanel/ │
│ (ROOT) │ │ │ │ │
│ Franchise Admin │ │Super Admin│ │ End User │
│ Workspace │ │ System │ │ Portal │
│ │ │ │ │ │
│ owner, staff │ │super_admin│ │member, student, │
│ │ │ │ │customer, patient│
└─────────────────┘ └───────────┘ └─────────────────┘重要说明:根目录是加盟商管理员工作区,而非用户面板!
/public/File Structure Convention
文件结构规范
public/
├── index.php # Landing page with nav buttons (NOT a router)
├── sign-in.php # Login with SweetAlert
├── dashboard.php # Franchise admin dashboard
├── skeleton.php # Page template for new pages
├── adminpanel/ # Super admin panel
│ ├── index.php
│ └── includes/ # Admin-specific includes
├── memberpanel/ # End user portal
│ ├── index.php
│ └── includes/ # Member-specific includes
├── includes/ # Shared includes for /public/ root
├── assets/ # Shared CSS/JS
└── uploads/ # File uploadspublic/
├── index.php # 带导航按钮的登录页(非路由)
├── sign-in.php # 集成SweetAlert的登录页
├── dashboard.php # 加盟商管理员仪表盘
├── skeleton.php # 新页面模板
├── adminpanel/ # 超级管理员面板
│ ├── index.php
│ └── includes/ # 管理员专属依赖文件
├── memberpanel/ # 终端用户门户
│ ├── index.php
│ └── includes/ # 用户专属依赖文件
├── includes/ # `/public/`根目录的共享依赖文件
├── assets/ # 共享CSS/JS资源
└── uploads/ # 文件上传目录1. Franchise Admin Panel (/public/
root) - THE MAIN WORKSPACE
/public/1. 加盟商管理面板(/public/
根目录)—— 主工作区
/public/Purpose: Daily franchise operations (NOT member portal!)
Location: , , etc.
Users: Franchise owners, managers, staff
User Types: ,
Auth: Session-based (web), JWT (mobile/API)
Scope: Single franchise only, cannot access other franchises
/public/dashboard.php/public/students.phpownerstaffKey Constraints:
- All queries include
WHERE franchise_id = ? - Cannot modify platform settings
- Cannot create/suspend other franchises
- All operations logged for franchise audit
Example Pages:
- - Franchise admin dashboard
/public/dashboard.php - - Manage students (school SaaS)
/public/students.php - - Manage inventory (restaurant SaaS)
/public/inventory.php - - Manage patients (medical SaaS)
/public/patients.php
用途: 加盟商日常运营(非用户门户!)
位置: , 等
用户: 加盟商所有者、管理者、员工
用户类型: ,
认证方式: 基于Session(Web端)、JWT(移动端/API)
范围: 仅单个加盟商,无法访问其他加盟商数据
/public/dashboard.php/public/students.phpownerstaff关键约束:
- 所有查询必须包含
WHERE franchise_id = ? - 无法修改平台设置
- 无法创建/暂停其他加盟商
- 所有操作都会记录到加盟商审计日志
示例页面:
- - 加盟商管理员仪表盘
/public/dashboard.php - - 管理学生(教育类SaaS)
/public/students.php - - 管理库存(餐饮类SaaS)
/public/inventory.php - - 管理患者(医疗类SaaS)
/public/patients.php
2. Super Admin Panel (/public/adminpanel/
)
/public/adminpanel/2. 超级管理员面板(/public/adminpanel/
)
/public/adminpanel/Purpose: Platform management and oversight
Location:
Users: Super admins, platform operators
User Type:
Auth: Session-based + MFA recommended
Scope: Cross-franchise with audit trails
/public/adminpanel/super_adminCapabilities:
- Create/suspend franchises
- Manage platform users
- View cross-franchise analytics
- Configure platform settings
- Access all franchise data (logged)
Critical Rules:
- Every action creates audit log
- Production data access logged
- franchise_id can be NULL for super admins
- Can impersonate franchise users (logged)
用途: 平台管理与监督
位置:
用户: 超级管理员、平台运营人员
用户类型:
认证方式: 基于Session + 推荐MFA(多因素认证)
范围: 跨加盟商,带审计追踪
/public/adminpanel/super_admin功能:
- 创建/暂停加盟商
- 管理平台用户
- 查看跨加盟商分析数据
- 配置平台设置
- 访问所有加盟商数据(操作会被记录)
重要规则:
- 每一个操作都会生成审计日志
- 生产环境数据访问会被记录
- 超级管理员的franchise_id可以为NULL
- 可以模拟加盟商用户(操作会被记录)
3. End User Portal (/public/memberpanel/
)
/public/memberpanel/3. 终端用户门户(/public/memberpanel/
)
/public/memberpanel/Purpose: Self-service for end users
Location:
Users: End customers/patients/students (outside franchise staff)
User Types: , , , (customizable)
Auth: Session-based or JWT
Scope: Own records only, read-mostly
/public/memberpanel/memberstudentcustomerpatientExamples:
- Student portal - View grades, assignments
- Customer portal - Order tracking, invoices
- Patient portal - View medical records, appointments
- Member portal - Self-service access
用途: 终端用户自助服务
位置:
用户: 终端客户/患者/学生(加盟商员工以外的用户)
用户类型: , , , (可自定义)
认证方式: 基于Session或JWT
范围: 仅能访问自身记录,以只读操作为主
/public/memberpanel/memberstudentcustomerpatient示例:
- 学生门户 - 查看成绩、作业
- 客户门户 - 订单追踪、发票
- 患者门户 - 查看病历、预约
- 用户门户 - 自助服务访问
Franchise Isolation Model (Multi-Tenant)
加盟商隔离模型(多租户)
Terminology: We use instead of in SaaS Seeder Template.
franchisetenant术语说明: 在SaaS Seeder模板中,我们使用代替。
franchisetenantUser Types & Franchise Requirements
用户类型与加盟商要求
CRITICAL: Understand franchise_id requirements per user type:
super_admin - Platform operators (franchise_id CAN be NULL)
owner - Franchise owners (franchise_id REQUIRED, NOT NULL)
staff - Franchise staff (franchise_id REQUIRED, NOT NULL)
member - End users: student, customer, patient (franchise_id REQUIRED, NOT NULL)重要说明: 理解不同用户类型对franchise_id的要求:
super_admin - 平台运营人员(franchise_id 可以为NULL)
owner - 加盟商所有者(franchise_id 为必填项,不可为NULL)
staff - 加盟商员工(franchise_id 为必填项,不可为NULL)
member - 终端用户:学生、客户、患者(franchise_id 为必填项,不可为NULL)Database-Level Isolation
数据库级隔离
Option 1: Shared Database (Row-Level franchise_id) ← SaaS Seeder Template Uses This
sql
-- Every franchise-scoped table has franchise_id
CREATE TABLE students (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
franchise_id BIGINT UNSIGNED NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(100),
-- other fields
FOREIGN KEY (franchise_id) REFERENCES tbl_franchises(id)
ON DELETE CASCADE,
INDEX idx_franchise (franchise_id),
INDEX idx_franchise_email (franchise_id, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ALL queries MUST include franchise_id
SELECT * FROM students WHERE franchise_id = ? AND id = ?;Option 2: Schema-Per-Franchise
sql
-- PostgreSQL: Separate schema per franchise
CREATE SCHEMA franchise_123;
CREATE TABLE franchise_123.students (...);Option 3: Database-Per-Franchise (High isolation, ops overhead)
Recommendation: Start with Option 1 (row-level), migrate to Option 2 for large/regulated franchises.
SaaS Seeder Template Convention:
- Table prefix: for shared tables (users, franchises, roles)
tbl_ - No prefix: For franchise-scoped data (students, orders, inventory)
- Collation: for all text columns
utf8mb4_unicode_ci - Charset: for emoji and international character support
utf8mb4
方案1:共享数据库(行级franchise_id隔离) ← SaaS Seeder模板采用此方案
sql
-- 所有加盟商范围的表都包含franchise_id
CREATE TABLE students (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
franchise_id BIGINT UNSIGNED NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(100),
-- 其他字段
FOREIGN KEY (franchise_id) REFERENCES tbl_franchises(id)
ON DELETE CASCADE,
INDEX idx_franchise (franchise_id),
INDEX idx_franchise_email (franchise_id, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 所有查询必须包含franchise_id
SELECT * FROM students WHERE franchise_id = ? AND id = ?;方案2:按加盟商分Schema
sql
-- PostgreSQL:每个加盟商对应独立Schema
CREATE SCHEMA franchise_123;
CREATE TABLE franchise_123.students (...);方案3:按加盟商分数据库(隔离度高,但运维成本大)
推荐方案: 从方案1(行级隔离)开始,针对大型/受监管的加盟商迁移到方案2。
SaaS Seeder模板规范:
- 表前缀:共享表(用户、加盟商、角色)使用前缀
tbl_ - 无前缀:加盟商范围的数据表(学生、订单、库存)
- 排序规则:所有文本列使用
utf8mb4_unicode_ci - 字符集:,支持表情符号和国际字符
utf8mb4
Application-Level Enforcement
应用层强制隔离
PHP Pattern (Session-based with prefix system):
php
// Extract franchise context from session (with prefix)
$franchiseId = getSession('franchise_id'); // Uses SESSION_PREFIX
$userType = getSession('user_type');
// ALWAYS filter by franchise_id
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([$franchiseId, $studentId]);
// For super_admin, allow cross-franchise access (logged)
if ($userType === 'super_admin') {
// Can access any franchise, but log the action
auditLog('CROSS_FRANCHISE_ACCESS', [
'admin_user_id' => getSession('user_id'),
'target_franchise_id' => $requestedFranchiseId,
'action' => 'VIEW_STUDENTS'
]);
// Query without franchise filter (super admin only)
$stmt = $db->prepare("SELECT * FROM students WHERE id = ?");
$stmt->execute([$studentId]);
} else {
// Regular users: MUST filter by their franchise_id
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([getSession('franchise_id'), $studentId]);
}JavaScript Pattern (JWT-based):
javascript
// Extract franchise context from JWT
function extractFranchiseContext(req) {
const token = verifyJWT(req.headers.authorization);
return {
userId: token.sub,
franchiseId: token.fid, // franchise_id in token
userType: token.ut, // user_type in token
};
}
// Enforce franchise scope on all queries
function scopeQuery(query, franchiseId) {
if (!franchiseId) throw new Error("Missing franchise context");
return query.where("franchise_id", franchiseId);
}Critical: Never trust client-provided franchise_id
php
// BAD - Client controls franchise_id
$franchiseId = $_POST['franchise_id']; // ❌ NEVER DO THIS!
// GOOD - Server extracts from session (with prefix)
$franchiseId = getSession('franchise_id'); // ✅
// GOOD - Server extracts from JWT
$franchiseId = $jwtPayload->fid; // ✅PHP模式(基于Session的前缀系统):
php
// 从Session中提取加盟商上下文(带前缀)
$franchiseId = getSession('franchise_id'); // 使用SESSION_PREFIX
$userType = getSession('user_type');
// 始终按franchise_id过滤
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([$franchiseId, $studentId]);
// 超级管理员允许跨加盟商访问(操作会被记录)
if ($userType === 'super_admin') {
// 可以访问任意加盟商,但操作会被记录
auditLog('CROSS_FRANCHISE_ACCESS', [
'admin_user_id' => getSession('user_id'),
'target_franchise_id' => $requestedFranchiseId,
'action' => 'VIEW_STUDENTS'
]);
// 超级管理员专属:查询无需加盟商过滤
$stmt = $db->prepare("SELECT * FROM students WHERE id = ?");
$stmt->execute([$studentId]);
} else {
// 普通用户:必须按自身franchise_id过滤
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([getSession('franchise_id'), $studentId]);
}JavaScript模式(基于JWT):
javascript
// 从JWT中提取加盟商上下文
function extractFranchiseContext(req) {
const token = verifyJWT(req.headers.authorization);
return {
userId: token.sub,
franchiseId: token.fid, // franchise_id in token
userType: token.ut, // user_type in token
};
}
// 对所有查询强制加盟商范围限制
function scopeQuery(query, franchiseId) {
if (!franchiseId) throw new Error("Missing franchise context");
return query.where("franchise_id", franchiseId);
}重要提示:永远不要信任客户端提供的franchise_id
php
// 错误示例 - 客户端控制franchise_id
$franchiseId = $_POST['franchise_id']; // ❌ 绝对不要这么做!
// 正确示例 - 从服务端Session(带前缀)提取
$franchiseId = getSession('franchise_id'); // ✅
// 正确示例 - 从JWT中提取
$franchiseId = $jwtPayload->fid; // ✅Authentication & Authorization
认证与授权
User Types
用户类型
SaaS Seeder Template User Types:
- - Platform management, cross-franchise (franchise_id CAN be NULL)
super_admin - - Full control within franchise (franchise_id REQUIRED)
owner - - Operational permissions within franchise (franchise_id REQUIRED)
staff - Custom end user types (franchise_id REQUIRED):
- - For school/education SaaS
student - - For e-commerce/restaurant SaaS
customer - - For medical/clinic SaaS
patient - - Generic end user
member
Customizing User Types:
sql
-- Edit database enum to match your SaaS domain
ALTER TABLE tbl_users MODIFY user_type ENUM(
'super_admin',
'owner',
'staff',
'student', -- School SaaS
'customer', -- Restaurant SaaS
'patient' -- Medical SaaS
) NOT NULL DEFAULT 'staff';SaaS Seeder模板用户类型:
- - 平台管理,跨加盟商(franchise_id 可以为NULL)
super_admin - - 加盟商内部完全控制(franchise_id 为必填项)
owner - - 加盟商内部操作权限(franchise_id 为必填项)
staff - 自定义终端用户类型(franchise_id 为必填项):
- - 教育类SaaS
student - - 电商/餐饮类SaaS
customer - - 医疗类SaaS
patient - - 通用终端用户
member
自定义用户类型:
sql
-- 修改数据库枚举以匹配你的SaaS领域
ALTER TABLE tbl_users MODIFY user_type ENUM(
'super_admin',
'owner',
'staff',
'student', -- 教育类SaaS
'customer', -- 餐饮类SaaS
'patient' -- 医疗类SaaS
) NOT NULL DEFAULT 'staff';Permission Model
权限模型
See for complete schema
references/permission-model.mdjavascript
// Permission resolution priority:
// 1. User denial (explicit deny) → DENY
// 2. User grant (explicit allow) → ALLOW
// 3. Franchise override → ALLOW/DENY
// 4. Role permission → ALLOW
// 5. Default → DENY
function hasPermission(userId, tenantId, permission) {
// Super admin bypass
if (user.type === "super_admin") return true;
// Check explicit denials
if (userPermissions.denied(userId, tenantId, permission)) return false;
// Check explicit grants
if (userPermissions.granted(userId, tenantId, permission)) return true;
// Check role-based permissions
const roles = getUserRoles(userId, tenantId);
for (const role of roles) {
if (roleHasPermission(role, permission, tenantId)) return true;
}
return false; // Default deny
}完整Schema请参考
references/permission-model.mdjavascript
// 权限解析优先级:
// 1. 用户明确拒绝 → 拒绝
// 2. 用户明确允许 → 允许
// 3. 加盟商覆盖规则 → 允许/拒绝
// 4. 角色权限 → 允许
// 5. 默认规则 → 拒绝
function hasPermission(userId, tenantId, permission) {
// 超级管理员绕过所有权限检查
if (user.type === "super_admin") return true;
// 检查明确拒绝的权限
if (userPermissions.denied(userId, tenantId, permission)) return false;
// 检查明确允许的权限
if (userPermissions.granted(userId, tenantId, permission)) return true;
// 检查基于角色的权限
const roles = getUserRoles(userId, tenantId);
for (const role of roles) {
if (roleHasPermission(role, permission, tenantId)) return true;
}
return false; // 默认拒绝
}Session Management
Session管理
Session Prefix System (Multi-Tenant Isolation):
CRITICAL: All session variables use a prefix to prevent collisions:
php
// Define in src/config/session.php
define('SESSION_PREFIX', 'saas_app_'); // Change per SaaS
// ALWAYS use helper functions
setSession('user_id', 123); // Sets $_SESSION['saas_app_user_id']
$userId = getSession('user_id'); // Gets $_SESSION['saas_app_user_id']
hasSession('user_id'); // Checks if exists
destroySession(); // Clears all prefixed vars
// Common session variables (with prefix):
setSession('user_id', $userId);
setSession('franchise_id', $franchiseId);
setSession('user_type', $userType);
setSession('username', $username);
setSession('full_name', $fullName);
setSession('last_activity', time());Customize prefix per SaaS:
php
define('SESSION_PREFIX', 'school_'); // School SaaS
define('SESSION_PREFIX', 'restaurant_'); // Restaurant SaaS
define('SESSION_PREFIX', 'clinic_'); // Medical SaaS
define('SESSION_PREFIX', 'hotel_'); // Hospitality SaaSWeb (Session-based):
HttpOnly: true
Secure: auto-detect HTTPS (allow localhost HTTP)
SameSite: Strict
Lifetime: 30 minutes
Regenerate on loginHTTPS Auto-Detection (Critical for Development):
php
// Only set secure cookie if using HTTPS
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| $_SERVER['SERVER_PORT'] == 443;
ini_set('session.cookie_secure', $isHttps ? '1' : '0');
// Without this, sessions won't persist on localhost HTTPAPI (JWT-based):
Access token: 15 minutes
Refresh token: 30 days
Rotation on refresh
Revocation table for logoutSession前缀系统(多租户隔离):
重要提示:所有Session变量使用前缀以避免冲突:
php
// 在src/config/session.php中定义
define('SESSION_PREFIX', 'saas_app_'); // 每个SaaS应用单独修改
// 始终使用辅助函数
setSession('user_id', 123); // 设置$_SESSION['saas_app_user_id']
$userId = getSession('user_id'); // 获取$_SESSION['saas_app_user_id']
hasSession('user_id'); // 检查是否存在
destroySession(); // 清除所有带前缀的变量
// 常见Session变量(带前缀):
setSession('user_id', $userId);
setSession('franchise_id', $franchiseId);
setSession('user_type', $userType);
setSession('username', $username);
setSession('full_name', $fullName);
setSession('last_activity', time());为每个SaaS应用自定义前缀:
php
define('SESSION_PREFIX', 'school_'); // 教育类SaaS
define('SESSION_PREFIX', 'restaurant_'); // 餐饮类SaaS
define('SESSION_PREFIX', 'clinic_'); // 医疗类SaaS
define('SESSION_PREFIX', 'hotel_'); // 酒店类SaaSWeb端(基于Session):
HttpOnly: true
Secure: 自动检测HTTPS(允许本地HTTP)
SameSite: Strict
有效期:30分钟
登录时重新生成SessionHTTPS自动检测(开发环境关键配置):
php
// 仅在HTTPS环境下设置Secure Cookie
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| $_SERVER['SERVER_PORT'] == 443;
ini_set('session.cookie_secure', $isHttps ? '1' : '0');
// 没有此配置,本地HTTP环境下Session无法持久化API端(基于JWT):
访问令牌:15分钟
刷新令牌:30天
刷新时轮换令牌
登出时使用吊销表Security Architecture
安全架构
Zero-Trust Checklist
零信任检查清单
Authentication:
- MFA for admin access
- Password: Argon2ID + salt + pepper
- Account lockout after 5 failures
- Session timeout (30 min idle)
- Token rotation on refresh
Authorization:
- Tenant context in every request
- Permission check before every operation
- Super admin actions audited
- Impersonation logged with justification
Data Access:
- in WHERE clause (ALWAYS)
tenant_id - Prepared statements (no SQL injection)
- Input validation at API boundary
- Output encoding (XSS prevention)
API Security:
- Rate limiting (per tenant, per user)
- CORS whitelist (no wildcards)
- Request size limits
- HTTPS only (HSTS enabled)
认证:
- 管理员访问启用MFA
- 密码:Argon2ID + 盐 + 胡椒
- 5次失败尝试后锁定账户
- Session闲置超时(30分钟)
- 刷新时轮换令牌
授权:
- 每个请求都包含租户上下文
- 操作前检查权限
- 超级管理员操作被审计
- 模拟用户操作需记录理由
数据访问:
- 查询中始终包含
tenant_id - 使用预编译语句(防止SQL注入)
- API边界处验证输入
- 输出编码(防止XSS)
API安全:
- 速率限制(按租户、按用户)
- CORS白名单(禁止通配符)
- 请求大小限制
- 仅允许HTTPS(启用HSTS)
Common Security Mistakes
常见安全错误
❌ Trusting client-provided franchise_id
php
// BAD - Client controls!
$franchiseId = $_POST['franchise_id'];
$stmt = $db->prepare("SELECT * FROM students WHERE franchise_id = ?");✅ Extract from server-side session (with prefix)
php
// GOOD - Server-side session with prefix
$franchiseId = getSession('franchise_id');
$stmt = $db->prepare("SELECT * FROM students WHERE franchise_id = ?");❌ Missing franchise_id in queries
php
// BAD - Missing franchise check! Data leakage!
$stmt = $db->prepare("SELECT * FROM students WHERE id = ?");
$stmt->execute([$studentId]);✅ Always include franchise scope
php
// GOOD - Always filter by franchise_id
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([getSession('franchise_id'), $studentId]);❌ Super admin without audit
php
if (getSession('user_type') === 'super_admin') {
// Direct action without logging
deleteStudent($studentId);
}✅ Super admin WITH audit
php
if (getSession('user_type') === 'super_admin') {
// Log cross-franchise access
auditLog('ADMIN_DELETE_STUDENT', [
'admin_user_id' => getSession('user_id'),
'target_franchise_id' => $franchiseId,
'student_id' => $studentId
]);
deleteStudent($studentId);
}❌ Not using session prefix system
php
// BAD - Direct session access (collision risk)
$_SESSION['user_id'] = $userId;
$userId = $_SESSION['user_id'];✅ Use session prefix helpers
php
// GOOD - Prefixed session (namespace isolation)
setSession('user_id', $userId);
$userId = getSession('user_id');❌ 信任客户端提供的franchise_id
php
// 错误示例 - 客户端控制!
$franchiseId = $_POST['franchise_id'];
$stmt = $db->prepare("SELECT * FROM students WHERE franchise_id = ?");✅ 从服务端Session(带前缀)提取
php
// 正确示例 - 带前缀的服务端Session
$franchiseId = getSession('franchise_id');
$stmt = $db->prepare("SELECT * FROM students WHERE franchise_id = ?");❌ 查询中缺失franchise_id
php
// 错误示例 - 缺失加盟商检查!数据泄露风险!
$stmt = $db->prepare("SELECT * FROM students WHERE id = ?");
$stmt->execute([$studentId]);✅ 始终包含加盟商范围限制
php
// 正确示例 - 始终按franchise_id过滤
$stmt = $db->prepare("
SELECT * FROM students
WHERE franchise_id = ? AND id = ?
");
$stmt->execute([getSession('franchise_id'), $studentId]);❌ 超级管理员操作未审计
php
if (getSession('user_type') === 'super_admin') {
// 直接操作未记录
deleteStudent($studentId);
}✅ 超级管理员操作需审计
php
if (getSession('user_type') === 'super_admin') {
// 记录跨加盟商访问
auditLog('ADMIN_DELETE_STUDENT', [
'admin_user_id' => getSession('user_id'),
'target_franchise_id' => $franchiseId,
'student_id' => $studentId
]);
deleteStudent($studentId);
}❌ 未使用Session前缀系统
php
// 错误示例 - 直接访问Session(存在冲突风险)
$_SESSION['user_id'] = $userId;
$userId = $_SESSION['user_id'];✅ 使用Session前缀辅助函数
php
// 正确示例 - 带前缀的Session(命名空间隔离)
setSession('user_id', $userId);
$userId = getSession('user_id');API Design Principles
API设计原则
Tenant Context in Requests
请求中的租户上下文
Option 1: Subdomain
https://tenant-slug.yourapp.com/api/v1/ordersOption 2: Path parameter
https://api.yourapp.com/v1/tenants/{tenant_id}/ordersOption 3: Header
X-Tenant-ID: 123
Authorization: Bearer <token>Recommendation: Use JWT with claim (no client input).
tenant_id方案1:子域名
https://tenant-slug.yourapp.com/api/v1/orders方案2:路径参数
https://api.yourapp.com/v1/tenants/{tenant_id}/orders方案3:请求头
X-Tenant-ID: 123
Authorization: Bearer <token>推荐方案: 使用包含声明的JWT(无需客户端输入)。
tenant_idRESTful Conventions
RESTful规范
GET /api/v1/orders → List (tenant-scoped)
POST /api/v1/orders → Create (tenant-scoped)
GET /api/v1/orders/{id} → Show (tenant-scoped)
PUT /api/v1/orders/{id} → Update (tenant-scoped)
DELETE /api/v1/orders/{id} → Delete (tenant-scoped)
// Admin endpoints (cross-tenant)
GET /api/v1/admin/tenants → List all tenants
POST /api/v1/admin/tenants → Create tenant
GET /api/v1/admin/analytics → Cross-tenant analytics
POST /api/v1/admin/impersonate → Start impersonationGET /api/v1/orders → 列表(租户范围)
POST /api/v1/orders → 创建(租户范围)
GET /api/v1/orders/{id} → 详情(租户范围)
PUT /api/v1/orders/{id} → 更新(租户范围)
DELETE /api/v1/orders/{id} → 删除(租户范围)
// 管理员端点(跨租户)
GET /api/v1/admin/tenants → 查看所有租户
POST /api/v1/admin/tenants → 创建租户
GET /api/v1/admin/analytics → 跨租户分析
POST /api/v1/admin/impersonate → 开始模拟用户Response Format
响应格式
json
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"per_page": 25,
"total": 100
}
}Error format:
json
{
"success": false,
"error": {
"code": "PERMISSION_DENIED",
"message": "You do not have permission to access this resource",
"details": {}
}
}json
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"per_page": 25,
"total": 100
}
}错误格式:
json
{
"success": false,
"error": {
"code": "PERMISSION_DENIED",
"message": "You do not have permission to access this resource",
"details": {}
}
}Audit & Compliance
审计与合规
What to Audit
审计内容
Always log:
- Super admin actions (ALL)
- Impersonation start/end
- Permission changes
- Tenant creation/suspension
- Data exports
- Failed auth attempts
- Cross-tenant access attempts (should be 0)
Audit record format:
json
{
"id": "uuid",
"timestamp": "2025-01-23T10:30:00Z",
"actor_user_id": 123,
"actor_type": "super_admin",
"action": "IMPERSONATE_USER",
"target_tenant_id": 456,
"target_user_id": 789,
"justification": "Customer support request #12345",
"ip_address": "203.0.113.1",
"user_agent": "...",
"changes": { "before": {...}, "after": {...} }
}Retention:
- Security logs: 1 year minimum
- Audit trails: 7 years (compliance)
- Operational logs: 90 days
必须记录:
- 超级管理员的所有操作
- 模拟用户的开始/结束
- 权限变更
- 租户创建/暂停
- 数据导出
- 认证失败尝试
- 跨租户访问尝试(应保持为0)
审计记录格式:
json
{
"id": "uuid",
"timestamp": "2025-01-23T10:30:00Z",
"actor_user_id": 123,
"actor_type": "super_admin",
"action": "IMPERSONATE_USER",
"target_tenant_id": 456,
"target_user_id": 789,
"justification": "Customer support request #12345",
"ip_address": "203.0.113.1",
"user_agent": "...",
"changes": { "before": {...}, "after": {...} }
}保留期限:
- 安全日志:至少1年
- 审计追踪:7年(合规要求)
- 运营日志:90天
Operational Safeguards
运营保障
Tenant Lifecycle
租户生命周期
PENDING → ACTIVE → SUSPENDED → ARCHIVED- PENDING: Created, not yet activated
- ACTIVE: Normal operations
- SUSPENDED: Payment failure, ToS violation (data retained)
- ARCHIVED: Deleted (data purged after retention period)
PENDING → ACTIVE → SUSPENDED → ARCHIVED- PENDING:已创建,尚未激活
- ACTIVE:正常运营
- SUSPENDED:支付失败、违反服务条款(数据保留)
- ARCHIVED:已删除(保留期后清除数据)
Data Protection
数据保护
Backups:
- Daily automated backups
- Point-in-time recovery (30 days)
- Test restore quarterly
- Tenant-level restore capability
Encryption:
- At rest: AES-256
- In transit: TLS 1.3
- Sensitive fields: Application-level encryption (PII, payment)
备份:
- 每日自动备份
- 时间点恢复(30天)
- 每季度测试恢复
- 支持按租户恢复
加密:
- 静态数据:AES-256
- 传输中数据:TLS 1.3
- 敏感字段:应用层加密(个人身份信息、支付数据)
Rate Limiting
速率限制
Per tenant: 1000 req/min
Per user: 100 req/min
Per IP: 500 req/min
Admin endpoints: 50 req/min按租户:1000 请求/分钟
按用户:100 请求/分钟
按IP:500 请求/分钟
管理员端点:50 请求/分钟Monitoring Alerts
监控告警
Critical:
- Cross-tenant access attempt
- Super admin login from new IP
- Failed auth spike (>100/min)
- Database query without tenant_id
- API error rate >5%
关键告警:
- 跨租户访问尝试
- 超级管理员从新IP登录
- 认证失败激增(>100次/分钟)
- 未包含tenant_id的数据库查询
- API错误率>5%
Development Guidelines
开发指南
Code Review Checklist
代码审查清单
Every feature must:
- Include in all queries
tenant_id - Validate permissions before operations
- Create audit log for privileged actions
- Test cross-tenant isolation
- Handle tenant suspension state
- Document permission requirements
每个功能必须:
- 所有查询包含
tenant_id - 操作前验证权限
- 特权操作生成审计日志
- 测试跨租户隔离
- 处理租户暂停状态
- 文档化权限要求
Testing Requirements
测试要求
javascript
describe("Order API", () => {
it("prevents cross-tenant data access", async () => {
const tenant1Order = await createOrder(tenant1);
const tenant2User = await authenticateAs(tenant2.user);
const response = await tenant2User.get(`/orders/${tenant1Order.id}`);
expect(response.status).toBe(404); // Not 403 (info leak)
});
it("requires permission for operation", async () => {
const user = await authenticateAs(limitedUser);
const response = await user.delete("/orders/123");
expect(response.status).toBe(403);
expect(response.body.error.code).toBe("PERMISSION_DENIED");
});
});javascript
describe("Order API", () => {
it("prevents cross-tenant data access", async () => {
const tenant1Order = await createOrder(tenant1);
const tenant2User = await authenticateAs(tenant2.user);
const response = await tenant2User.get(`/orders/${tenant1Order.id}`);
expect(response.status).toBe(404); // 不要返回403(避免信息泄露)
});
it("requires permission for operation", async () => {
const user = await authenticateAs(limitedUser);
const response = await user.delete("/orders/123");
expect(response.status).toBe(403);
expect(response.body.error.code).toBe("PERMISSION_DENIED");
});
});Migration Patterns
迁移模式
See for adding tenant_id to existing tables
documentation/migration.md为现有表添加tenant_id请参考
documentation/migration.mdSummary
总结
Critical Implementation Rules:
- Franchise Isolation: in EVERY query (except super_admin with audit)
franchise_id - Auth Context: Extract franchise from session/JWT (never client input)
- Session Prefix: Use /
setSession()helpers (namespace isolation)getSession() - User Types: Understand franchise_id requirements (NULL only for super_admin)
- Permissions: Check before EVERY operation
- Audit: Log ALL privileged/cross-franchise actions
- Super Admin: Audit + MFA + IP restrictions
- Testing: Cross-franchise isolation tests mandatory
- Monitoring: Alert on cross-franchise access attempts
Architecture Patterns:
- Three-tier panel structure (CORE concept):
- root = Franchise admin workspace (NOT member panel!)
/public/ - = Super admin system
/adminpanel/ - = End user portal
/memberpanel/
- Session prefix system for multi-tenant isolation
- Zero-trust security model
- Row-level franchise isolation (start here)
- Role-based permissions with overrides
- Immutable audit trails
SaaS Seeder Template Specifics:
- Session prefix: (customize per SaaS)
saas_app_ - Password hashing: Argon2ID + salt(32 chars) + pepper(64+ chars)
- Use to create admin users (correct hashing)
super-user-dev.php - HTTPS auto-detection for session.cookie_secure (localhost development)
- Collation: for all text columns
utf8mb4_unicode_ci
See Also:
- - Complete three-tier architecture guide
../../docs/PANEL-STRUCTURE.md - - Development guidelines and common pitfalls
../../CLAUDE.md - - Complete database design, indexes, partitioning
references/database-schema.md - - RBAC implementation, caching, middleware
references/permission-model.md - - Adding franchise_id, zero-downtime migrations, rollback
documentation/migration.md
Remember: Security failures in multi-tenant systems affect ALL franchises. Test isolation exhaustively.
关键实施规则:
- 加盟商隔离: 所有查询必须包含(超级管理员操作需审计的情况除外)
franchise_id - 认证上下文: 从Session/JWT中提取加盟商信息(绝不使用客户端输入)
- Session前缀: 使用/
setSession()辅助函数(命名空间隔离)getSession() - 用户类型: 理解不同用户类型对franchise_id的要求(仅超级管理员可为NULL)
- 权限: 每一个操作前都要检查权限
- 审计: 记录所有特权/跨加盟商操作
- 超级管理员: 审计 + MFA + IP限制
- 测试: 必须测试跨租户隔离
- 监控: 对跨租户访问尝试触发告警
架构模式:
- 三面板结构(核心概念):
- 根目录 = 加盟商管理员工作区(非用户面板!)
/public/ - = 超级管理员系统
/adminpanel/ - = 终端用户门户
/memberpanel/
- 多租户隔离的Session前缀系统
- 零信任安全模型
- 行级加盟商隔离(初始方案)
- 带覆盖规则的基于角色的权限
- 不可变审计追踪
SaaS Seeder模板细节:
- Session前缀:(每个SaaS应用自定义)
saas_app_ - 密码哈希:Argon2ID + 盐(32字符) + 胡椒(64+字符)
- 使用创建管理员用户(正确哈希)
super-user-dev.php - Session.cookie_secure的HTTPS自动检测(本地开发环境)
- 所有文本列使用排序规则
utf8mb4_unicode_ci
参考文档:
- - 完整三面板架构指南
../../docs/PANEL-STRUCTURE.md - - 开发指南与常见陷阱
../../CLAUDE.md - - 完整数据库设计、索引、分区
references/database-schema.md - - RBAC实现、缓存、中间件
references/permission-model.md - - 添加franchise_id、零停机迁移、回滚
documentation/migration.md
注意: 多租户系统中的安全故障会影响所有加盟商。请全面测试隔离机制。