多租户SaaS系统概述
多租户架构是SaaS(Software as a Service)应用的核心设计模式,它允许单个应用实例为多个客户(租户)提供服务,同时保持数据隔离和个性化配置。本文将详细介绍如何使用ThinkPHP 6.0构建一个完整的多租户SaaS系统。
系统架构设计
我们采用基于数据库隔离的多租户方案,每个租户拥有独立的数据表或数据库,确保数据完全隔离。系统主要包含以下模块:
- 租户管理:租户注册、配置和管理
- 用户认证:多租户下的用户登录和权限控制
- 数据隔离:自动路由到对应租户数据库
- 账单系统:租户费用管理和订阅计划
环境准备与Composer配置
首先创建ThinkPHP 6.0项目并安装必要扩展:
# 创建ThinkPHP项目 composer create-project topthink/think saas-app cd saas-app # 安装多租户扩展 composer require luoxiaojiao/think-multi-app composer require topthink/think-migration # 安装其他依赖 composer require firebase/php-jwt composer require ramsey/uuid
数据库设计与迁移
创建系统核心表结构:
<?php // database/migrations/20230101000000_create_tenant_tables.php use thinkmigrationMigrator; use thinkmigrationdbColumn; class CreateTenantTables extends Migrator { public function change() { // 租户表 $table = $this->table('tenants', ['comment' => '租户表']); $table->addColumn('name', 'string', ['limit' => 100, 'comment' => '租户名称']) ->addColumn('subdomain', 'string', ['limit' => 50, 'comment' => '子域名']) ->addColumn('database_name', 'string', ['limit' => 100, 'comment' => '数据库名']) ->addColumn('status', 'integer', ['limit' => 1, 'default' => 1, 'comment' => '状态:1激活,0禁用']) ->addColumn('plan_type', 'string', ['limit' => 20, 'default' => 'basic', 'comment' => '套餐类型']) ->addColumn('expires_at', 'datetime', ['null' => true, 'comment' => '过期时间']) ->addTimestamps() ->addIndex(['subdomain'], ['unique' => true]) ->create(); // 租户用户表(主数据库) $table = $this->table('tenant_users', ['comment' => '租户用户表']); $table->addColumn('tenant_id', 'integer', ['comment' => '租户ID']) ->addColumn('username', 'string', ['limit' => 50, 'comment' => '用户名']) ->addColumn('email', 'string', ['limit' => 100, 'comment' => '邮箱']) ->addColumn('password', 'string', ['limit' => 255, 'comment' => '密码']) ->addColumn('is_super_admin', 'integer', ['limit' => 1, 'default' => 0, 'comment' => '是否超级管理员']) ->addTimestamps() ->addIndex(['tenant_id', 'email'], ['unique' => true]) ->create(); } } ?>
多租户中间件实现
创建中间件来识别租户并切换数据库连接:
<?php // app/middleware/TenantMiddleware.php namespace appmiddleware; use thinkfacadeDb; use thinkfacadeConfig; use appmodelTenant; class TenantMiddleware { public function handle($request, Closure $next) { // 从子域名获取租户标识 $subdomain = $this->getSubdomain($request->host()); if ($subdomain && $subdomain != 'www') { // 查询租户信息 $tenant = Tenant::where('subdomain', $subdomain) ->where('status', 1) ->find(); if ($tenant) { // 设置租户上下文 $request->tenant = $tenant; // 切换数据库配置 $this->switchTenantDatabase($tenant); return $next($request); } } // 非租户访问,跳转到主站或展示错误 return redirect('/'); } protected function getSubdomain($host) { $parts = explode('.', $host); if (count($parts) > 2) { return $parts[0]; } return null; } protected function switchTenantDatabase($tenant) { $config = Config::get('database.connections.mysql'); $config['database'] = $tenant->database_name; Config::set(['connections' => ['tenant' => $config]], 'database'); Db::connect('tenant')->connect(); } } ?>
租户模型与业务逻辑
实现租户模型和相关业务方法:
<?php // app/model/Tenant.php namespace appmodel; use thinkModel; use thinkfacadeDb; use thinkfacadeConfig; use RamseyUuidUuid; class Tenant extends Model { protected $autoWriteTimestamp = true; // 创建新租户 public static function createTenant($data) { $tenant = new self(); $tenant->name = $data['name']; $tenant->subdomain = $data['subdomain']; $tenant->database_name = 'tenant_' . Uuid::uuid4()->toString(); $tenant->plan_type = $data['plan_type'] ?? 'basic'; if ($tenant->save()) { // 创建租户数据库 self::createTenantDatabase($tenant->database_name); // 初始化租户数据表 self::initTenantTables($tenant->database_name); return $tenant; } return false; } // 创建租户数据库 protected static function createTenantDatabase($databaseName) { $config = Config::get('database.connections.mysql'); $tempDb = Db::connect($config); $tempDb->execute("CREATE DATABASE IF NOT EXISTS `{$databaseName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); } // 初始化租户表结构 protected static function initTenantTables($databaseName) { $config = Config::get('database.connections.mysql'); $config['database'] = $databaseName; $tenantDb = Db::connect($config); // 创建租户业务表 $tenantDb->execute(" CREATE TABLE IF NOT EXISTS `users` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(100) NOT NULL, `email` VARCHAR(100) UNIQUE NOT NULL, `password` VARCHAR(255) NOT NULL, `created_at` DATETIME, `updated_at` DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); // 可以继续创建其他业务表... } // 获取租户数据库连接 public function getDatabaseConnection() { $config = Config::get('database.connections.mysql'); $config['database'] = $this->database_name; return Db::connect($config); } } ?>
多租户用户认证
实现跨租户的用户认证系统:
<?php // app/service/TenantAuth.php namespace appservice; use thinkFacade; use thinkfacadeSession; use appmodelTenantUser; class TenantAuth extends Facade { protected static function getFacadeClass() { return 'app\service\auth\TenantAuthService'; } } // app/service/auth/TenantAuthService.php namespace appserviceauth; use thinkfacadeDb; use thinkfacadeRequest; use appmodelTenantUser; class TenantAuthService { protected $tenant; protected $user; public function __construct() { $this->tenant = Request::tenant; } // 租户用户登录 public function login($email, $password) { $user = TenantUser::where('email', $email) ->where('tenant_id', $this->tenant->id) ->find(); if ($user && password_verify($password, $user->password)) { Session::set('tenant_user', [ 'id' => $user->id, 'tenant_id' => $user->tenant_id, 'email' => $user->email ]); return true; } return false; } // 获取当前登录用户 public function user() { if (!$this->user) { $sessionUser = Session::get('tenant_user'); if ($sessionUser) { $this->user = TenantUser::find($sessionUser['id']); } } return $this->user; } // 检查是否登录 public function check() { return !is_null($this->user()); } // 退出登录 public function logout() { Session::delete('tenant_user'); $this->user = null; } // 注册新用户 public function register($data) { $user = new TenantUser(); $user->tenant_id = $this->tenant->id; $user->username = $data['username']; $user->email = $data['email']; $user->password = password_hash($data['password'], PASSWORD_DEFAULT); return $user->save(); } } ?>
租户控制器实现
创建租户相关的控制器:
<?php // app/controller/tenant/Dashboard.php namespace appcontrollertenant; use appBaseController; use appserviceTenantAuth; class Dashboard extends BaseController { public function index() { if (!TenantAuth::check()) { return redirect('/tenant/login'); } $user = TenantAuth::user(); return view('tenant/dashboard', [ 'user' => $user, 'tenant' => request()->tenant ]); } } // app/controller/tenant/Auth.php namespace appcontrollertenant; use appBaseController; use appserviceTenantAuth; use thinkfacadeView; class Auth extends BaseController { public function login() { if (request()->isPost()) { $email = input('email'); $password = input('password'); if (TenantAuth::login($email, $password)) { return redirect('/tenant/dashboard'); } return View::fetch('tenant/login', [ 'error' => '登录失败,请检查邮箱和密码' ]); } return View::fetch('tenant/login'); } public function register() { if (request()->isPost()) { $data = input(); if (TenantAuth::register($data)) { TenantAuth::login($data['email'], $data['password']); return redirect('/tenant/dashboard'); } return View::fetch('tenant/register', [ 'error' => '注册失败,请重试' ]); } return View::fetch('tenant/register'); } public function logout() { TenantAuth::logout(); return redirect('/tenant/login'); } } ?>
路由配置
配置多租户路由规则:
<?php // route/app.php use thinkfacadeRoute; use appmiddlewareTenantMiddleware; // 租户路由组 Route::group('tenant', function () { Route::get('dashboard', 'tenant.Dashboard/index'); Route::get('login', 'tenant.Auth/login'); Route::post('login', 'tenant.Auth/login'); Route::get('register', 'tenant.Auth/register'); Route::post('register', 'tenant.Auth/register'); Route::get('logout', 'tenant.Auth/logout'); })->middleware(TenantMiddleware::class); // 主站路由 Route::get('/', 'Index/index'); Route::post('/api/tenants', 'api.Tenant/create'); ?>
视图模板示例
创建租户登录页面模板:
<!DOCTYPE html> <html> <head> <title>租户登录 - <?= request()->tenant->name ?></title> </head> <body> <h1><?= request()->tenant->name ?> 管理系统</h1> <?php if (isset($error)): ?> <div style="color: red;"><?= $error ?></div> <?php endif; ?> <form method="post"> <div> <label>邮箱:</label> <input type="email" name="email" required> </div> <div> <label>密码:</label> <input type="password" name="password" required> </div> <button type="submit">登录</button> </form> <p><a href="<?= url('/tenant/register') ?>" rel="external nofollow" >注册新账户</a></p> </body> </html>
部署与优化建议
生产环境部署注意事项:
- 使用Redis缓存租户配置信息,减少数据库查询
- 实现数据库连接池管理,避免连接数过多
- 配置子域名通配符解析(*.yourdomain.com)
- 实现租户数据备份和恢复机制
- 使用队列处理租户创建等耗时操作
- 实施监控和日志记录,跟踪系统性能
安全考虑
- 验证子域名格式,防止注入攻击
- 实施租户间数据隔离检查,避免越权访问
- 使用HTTPS保护数据传输安全
- 定期进行安全审计和漏洞扫描
- 实现租户数据导出和删除功能,满足GDPR要求
总结
通过本教程,我们构建了一个完整的ThinkPHP 6.0多租户SaaS系统,实现了:
- 基于子域名的租户识别和路由
- 动态数据库切换和数据隔离
- 多租户用户认证系统
- 完整的租户生命周期管理
- 可扩展的架构设计
这种架构非常适合构建需要为多个客户提供独立实例的SaaS应用,如CRM系统、项目管理工具、电子商务平台等。ThinkPHP 6.0的现代化特性和灵活架构使得开发这类复杂系统变得更加高效和可维护。