详解如何用PostgreSQL RLS将租户授权边界下沉到数据库层,配合策略设计、特权路径和测试实现纵深防御,规避应用层漏加filter的风险。
租户隔离应当在漏掉 filter、新建 API 路由和仓促重构时依然成立。PostgreSQL 行级安全将关键的授权边界移到了更接近数据的位置——但前提是租户模型、策略、特权路径和测试必须被一同设计。
共享 Schema 的 SaaS 应用通常在每个租户拥有的行上存储 tenant_id,然后 API 在每个查询中加上 WHERE tenant_id = ...。这一约定对查询规划和可读性有帮助,但作为安全边界它本身是脆弱的:一次被遗忘的谓词、过于宽泛的 repository 方法、后端任务或临时查询都可能越界。
PostgreSQL 行级安全(RLS)允许表附加策略,由策略决定某个数据库角色可以读取或修改哪些行。当 RLS 启用且没有适用策略时,PostgreSQL 采用默认拒绝行为。策略可以针对命令和角色,PostgreSQL 在处理现有行时评估 USING 表达式,而 WITH CHECK 则控制通过插入或更新创建的行。PostgreSQL 行安全文档和 CREATE POLICY 参考定义了当前的行为。
| 应用层查询 | 仍然按租户过滤,以表达清晰意图、生成有用的执行计划并缩小结果集 |
|---|---|
| 数据库策略 | RLS 独立拒绝落在认证主体允许租户范围之外的行 |
| 操作验证 | 跨租户测试和策略审计在真实角色下验证边界 |
RLS 是纵深防御,而非完整的授权系统。表级授权仍决定角色能否访问某个对象,而策略决定它能访问哪些行。速率限制、订阅规则、字段级限制和工作流权限可能需要单独的控件。Supabase 在其 Data API 安全指南中记录了这两层关系。
从一个稳定的租户标识符和一个权威的成员关系表开始。每个租户拥有的表都应携带一个非空租户键,并以外键指向租户记录。避免从可变标签、电子邮件域名、URL slug 或未经服务器验证的客户端提交 header 推断租户。
create table public.organizations (
id uuid primary key default gen_random_uuid(),
-- ...
);
create table public.organization_members (
organization_id uuid not null references public.organizations(id),
user_id uuid not null,
role text not null check (role in ('owner', 'admin', 'member')),
primary key (organization_id, user_id)
);
create table public.projects (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id),
created_by uuid not null
);
create index projects_organization_id_idx
on public.projects (organization_id);
即使租户可以通过一系列联接被发现,也要传播租户键。直接使用键使所有权一目了然、简化策略,并给 PostgreSQL 一个可索引的谓词。用外键保护其完整性,并确保子记录不能通过策略未检查的更新被转移到另一个租户。
关于共享表、独立 Schema 和独立数据库之间的更广泛选择,参见我们的多租户 SaaS 架构指南。RLS 在强化一个明确的数据模型而非弥补模糊的所有权时最为强大。
下面的示例使用 Supabase Auth 的 auth.uid() 来识别已登录用户。Supabase 建议在暴露 Schema 的每个表上启用 RLS,并指出未认证的 auth.uid() 调用返回 null。其 RLS 指南还建议针对认证角色并为策略列建立索引。
alter table public.projects enable row level security;
create policy "members can read organization projects"
on public.projects
for select
using (
(select auth.uid()) is not null
and exists (
select 1
from public.organization_members membership
where membership.organization_id = projects.organization_id
and membership.user_id = (select auth.uid())
)
);
create policy "members can create organization projects"
on public.projects
for insert
with check (
(select auth.uid()) is not null
and exists (
select 1
from public.organization_members membership
where membership.organization_id = projects.organization_id
and membership.user_id = (select auth.uid())
)
and created_by = (select auth.uid())
);
create policy "members can update organization projects"
on public.projects
for update
using (
(select auth.uid()) is not null
and exists (
select 1
from public.organization_members membership
where membership.organization_id = projects.organization_id
and membership.user_id = (select auth.uid())
)
)
with check (
organization_id = parent_organization_id
and created_by = (select auth.uid())
);
当读、写、更新和删除权限不同时,使用独立的策略。更新需要能同时看到旧行的可见性以及对拟议新行的权限;明确的 WITH CHECK 可防止用户通过修改 organization_id 来逃脱预期的边界。将 owner 或 billing 管理员等业务角色保留在可信的数据库记录或服务器管理的声明中,而非用户可编辑的个人资料元数据中。
策略设计规则:从认证身份和权威成员关系派生租户访问权。永远不要仅仅因为浏览器提交了某个租户 ID 就信任它。
关于策略周围的完整认证层,我们的 Next.js 和 Supabase 认证指南涵盖了会话、角色检查和服务器端验证。
PostgreSQL 超级用户、具有 BYPASSRLS 的角色以及通常的表所有者会绕过行级安全。当所有者也应受策略约束时,PostgreSQL 可以应用 FORCE ROW LEVEL SECURITY,但迁移和管理流程仍然需要一个精心设计的角色模型。
Supabase service-role 凭据可以绕过 RLS,绝不能暴露在浏览器或客户可控的环境中。为那些真正需要跨租户访问的狭义服务器作业保留这些凭据。验证每个作业的输入,记录操作的系统和租户范围,优先使用狭义的数据库函数或专用角色,而非赋予通用请求处理程序无限制的表访问权限。
SECURITY DEFINER 函数时固定安全的 search_path 并审查所有权授权谓词作为正常查询的一部分运行,因此 Schema 和索引设计很重要。为租户键和成员关系查找列建立索引。保持策略表达式稳定和可理解。继续在应用查询中包含明确的租户过滤:RLS 是执行边界,而查询谓词传达意图并可以帮助 planner 构建高效计划。
where organization_id = $1
对代表性数据使用 EXPLAIN (ANALYZE, BUFFERS),并使用与应用相同的非所有者角色。检查点查询、列表页、排序、分页和成员关系密集的路径。将复杂策略视为生产查询代码:测量它、审查它,并防止受保护表之间的意外递归。
RLS 无法修复耗尽的连接池或无界查询。将策略工作与我们的 Node.js 数据库连接池指南中的容量实践配对使用。
正向测试证明一个用户可以完成任务。隔离测试证明另一个用户不能查看或修改它。创建至少两个租户,各有不同用户和数据,通过与生产相同的数据库角色和认证上下文运行测试,并尝试跨边界执行每个操作。
将策略更改作为安全迁移进行测试。在 fixture 中捕获角色、身份声明、授权和预期结果,这样未来的重构就不能悄无声息地扩大访问范围。
清点每个接触租户数据的表、角色、视图、函数和服务。先添加租户键和索引,用验证回填它们,然后在暂存环境中使用类生产角色引入策略。比较执行前后的应用结果,包括后端作业和管理流程。
在实际操作中按小批次表分组部署。观察授权失败、空结果集、延迟、查询计划和支撑工作流。如果某个功能在强制执行后失败,修复其身份或访问契约;不要用宽泛的 permissive 策略来恢复功能——那会削弱隔离。
记录谁可以绕过 RLS、为什么、来自哪个运行时,以及该路径如何被测试。每当迁移添加暴露的表或更改成员关系语义时审查清点。
Endurance Softwares 帮助团队设计安全的 SaaS 应用,提供实用的 PostgreSQL 和 Supabase 数据模型、可靠的 API、生产测试和云交付。