site logo

Marico's space

坚不可摧的 SaaS:PostgreSQL 行级安全性在 Laravel 中的应用 🛡️

编程技术 2026-08-21 17:34:31 7

最近给一个企业级多租户系统做安全加固,踩了几个坑才把 PostgreSQL 行级安全性(RLS)跑通。这篇把核心思路和具体实现说清楚,少走弯路。

应用层租户隔离的致命缺陷

做 B2B SaaS 平台,数据隔离是头等大事。一旦租户 A 能看到租户 B 的财务数据,轻则面临 SOC 2、GDPR 合规处罚,重则丢失企业客户,信誉扫地。

大多数 Laravel 开发者会用应用层安全来解决这个问题——最典型的就是 Eloquent 全局作用域。只要在模型上套个 Trait,Laravel 会自动给所有查询加上 WHERE tenant_id = X 条件。功能是好功能,但藏着一个致命隐患:这套机制完全依赖框架和开发者的纪律性。一旦有人写了裸 DB::select()、新建模型时忘记加 Trait、或者在复杂报表任务里调用了 withoutGlobalScopes(),安全墙直接塌掉。应用层照常返回跨租户数据,数据库本身对租户规则一无所知,只能乖乖配合。

我们做企业平台的目标是让数据泄露在数学上不可能实现。办法是:把安全边界从应用代码移到数据库引擎层,用 PostgreSQL 行级安全性(RLS)来实现。

行级安全性(RLS)是什么

RLS 是 PostgreSQL 原生提供的核心特性,允许 DBA 在表级别定义严格的安全策略。一旦开启 RLS,数据库引擎会在每条 SELECTINSERTUPDATEDELETE 执行前进行拦截,检查策略条件。如果当前数据库会话不满足条件,相关行直接消失,哪怕有人执行 SELECT * FROM invoices;,PostgreSQL 也只返回当前租户的发票记录。数据隔离在最底层的基础设施层完成,物理层面阻止跨租户数据访问。

第一步:设计数据库策略

实现 RLS 需要用原生 SQL 来配置 PostgreSQL 表,Laravel 的标准 Blueprint 构建器不支持 RLS,所以要用 DB::statement() 直接跟 Postgres 交互。

invoices 表举例,让 PostgreSQL 去查找一个名为 app.current_tenant_id 的会话变量。如果这个变量和行的 tenant_id 匹配,用户就能看到;否则行被严格隐藏。


use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint; return new class extends Migration
{ public function up(): void { Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained(); $table->decimal('amount', 10, 2); $table->string('status'); $table->timestamps(); }); // 1. 在指定表上启用行级安全性 DB::statement('ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;'); // 2. 即使是表 owner 也强制执行 RLS(超级用户连接时至关重要) DB::statement('ALTER TABLE invoices FORCE ROW LEVEL SECURITY;'); // 3. 定义严格的安全策略 // 只有当行的 tenant_id 与活动数据库会话中设置的 'app.current_tenant_id' 匹配时, // 该行才是可见的/可编辑的。 DB::statement(" CREATE POLICY tenant_isolation_policy ON invoices USING (tenant_id = current_setting('app.current_tenant_id')::bigint); "); } public function down(): void { DB::statement('DROP POLICY IF EXISTS tenant_isolation_policy ON invoices;'); DB::statement('ALTER TABLE invoices DISABLE ROW LEVEL SECURITY;'); Schema::dropIfExists('invoices'); }
};

第二步:中间件注入上下文

数据库加固完毕之后,默认会拦截所有查询。如果现在直接跑 Invoice::all(),会返回空集合,因为 PostgreSQL 连接的会话里根本没有设置 app.current_tenant_id

需要在每个 HTTP 请求的最开始把这个变量注入到数据库会话里。用一个健壮的 Laravel 中间件来实现:根据子域名、请求头或用户会话识别租户,然后执行轻量级 SQL SET LOCAL 命令。


namespace App\Http\Middleware; use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response; class EnforceRowLevelSecurity
{ public function handle(Request $request, Closure $next): Response { // 1. 识别当前租户 // (实际项目中可能通过自定义域名或认证用户来解析) $tenantId = $request->user()?->tenant_id; if (!$tenantId) { // 如果没有租户上下文,清除设置 // RLS 会自动拦截对租户安全表的所有访问 DB::statement("SET LOCAL app.current_tenant_id = ''"); return $next($request); } // 2. 将租户 ID 注入到活动 PostgreSQL 连接会话 // 'LOCAL' 关键字确保该设置只在当前事务/请求期间有效, // 避免连接池污染 DB::statement("SET LOCAL app.current_tenant_id = '{$tenantId}'"); return $next($request); }
}

第三步:加固异步队列

实现 RLS 时最容易犯的架构错误在后台队列系统。HTTP 请求结束后,数据库会话关闭。当 Redis 队列处理器取走异步任务(比如生成月度财务报表 PDF)时,它运行在一个全新的、独立的数据库会话中,没有任何租户上下文。如果在 Job 里查询 invoices 表,Postgres 会直接拦截。

要构建一个坚不可摧的系统,后台任务必须在执行业务逻辑之前显式重新初始化 PostgreSQL 会话上下文。


namespace App\Jobs; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use App\Models\Invoice; class GenerateMonthlyReport implements ShouldQueue
{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public readonly int $tenantId ) {} public function handle(): void { // 1. 为这个特定的工作进程重新建立 RLS 安全边界 DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenantId}'"); // 2. 安全地执行业务逻辑 // Postgres 会在原生层强制执行隔离 $invoices = Invoice::where('status', 'paid')->get(); // 生成 PDF... }
}

工程收益和合规保障

从应用层全局作用域迁移到数据库层 RLS,是平台安全架构的质的飞跃。把租户隔离的责任直接交给 PostgreSQL,从数据安全模型中彻底消除了"人为错误"这个因素。开发者可以写原生 SQL、绕过 Eloquent、或者执行复杂的多表 join,都不用担心跨租户数据泄露。对于需要通过 SOC 2 或 HIPAA 合规审计的企业级 SaaS 平台来说,证明数据隔离在最底层基础设施层得到数学级别的强制执行,不仅仅是技术炫技——这是拿下企业大单的核心竞争力。