本节摘要:PostgreSQL 的权限是三层结构:角色(可继承、可组合)持有权限,权限按"库 → 模式 → 表 → 列 → 行"逐层收窄。行级安全(RLS)在引擎层给表挂策略表达式,查询自动拼上过滤条件,多租户隔离不再依赖应用代码的自觉。RLS 默认对表所有者不生效,这个口子必须知晓并封堵。
-- 一个只读报表角色,可被多人"成为" CREATE ROLE report_reader NOLOGIN; GRANT USAGE ON SCHEMA analytics TO report_reader; GRANT SELECT ON analytics.daily_sales TO report_reader; -- 应用账号继承它的权限 CREATE ROLE app_report LOGIN PASSWORD '...'; GRANT report_reader TO app_report;
要点三条:角色可以继承角色,形成权限树;PUBLIC 是全员角色,回收默认权限要显式对 PUBLIC 操作;数据库、模式、表是三道闸,缺一不可——最常见的失误是给了表权限却忘了模式上的 USAGE。
默认权限体系是"全给再收回",新表的授权要跟得上建表节奏:
-- 让今后新建的表自动可读,省去逐张授权 ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO report_reader;
传统多租户靠"每条查询都记得加 tenant_id 条件",漏一条就泄露。RLS 把这条防线钉在表上:
ALTER TABLE project ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON project USING (tenant_id = current_setting('app.tenant_id')::int);
此后任何查询(包括应用拼错的 SQL)都会被引擎自动追加过滤,读不到也改不到别的租户的行:
SET app.tenant_id = '42'; SELECT count(*) FROM project; -- 只数出租户 42 的行
策略有 USING(读过滤)与 WITH CHECK(写校验)两半,可以分开设:
CREATE POLICY own_rows ON project FOR UPDATE USING (owner = current_user) -- 只能改自己的行 WITH CHECK (owner = current_user); -- 也不能改成别人的

⚠️ 常见坑:RLS 默认不约束表的所有者与超级用户——用超级用户账号跑应用等于裸奔。正确姿势是应用使用普通角色,或对表显式加 FORCE ROW LEVEL SECURITY 让所有者也受策略管辖。
RLS 策略在查询规划期被拼进 WHERE,因此它的性能就是谓词的性能。策略表达式用不上索引时,每条查询都是全表过滤:
-- 策略谓词里的表达式若与索引列不完全一致,索引失效 CREATE POLICY tenant_idx_unfriendly ON project USING (tenant_id = current_setting('app.tenant_id')::int); -- 配套表达式索引让谓词可走索引 CREATE INDEX ON project ((current_setting('app.tenant_id')::int), tenant_id);
更省心的做法是把取租户封装成函数并标记稳定(STABLE),再对 tenant_id 建普通索引——规划器把稳定函数当常量求值后,普通索引即可命中。验证手段就是 EXPLAIN:
SET app.tenant_id = '42'; EXPLAIN SELECT count(*) FROM project; -- 期望看到 Index Scan / Index Only Scan 而非 Seq Scan + Filter
RLS 上线前的验收清单应当包含这一步:以真实应用角色的权限跑一遍核心查询的计划,确认策略没有把索引扫变成全表扫。
多条策略并存时默认是"或"语义(PERMISSIVE):任一策略放行即放行。要做"白名单再减黑名单"的精细控制,用 RESTRICTIVE 策略:
-- 基础放行:本租户可见 CREATE POLICY p_tenant ON project USING (tenant_id = current_setting('app.tenant_id')::int); -- 追加收紧:且行状态不是已归档 CREATE POLICY r_not_archived ON project AS RESTRICTIVE USING (status <> 'archived');
两条叠加后,可见集是交集——RESTRICTIVE 永远只能收窄不能放宽。另一个必须刻进设计的默认:启用 RLS 但一条策略都没建的表,对普通角色完全不可见(默认拒绝),而对所有者仍然全可见。这两条规则组合出一个常见翻车现场:开发用所有者账号测试一切正常,应用用普通账号上线后查无数据。测试矩阵必须包含"非所有者角色"这一行。
某多租户系统的 RLS 上线半年后被租户报告看到他人数据。复盘发现两层叠加:排障脚本用超级用户跑,RLS 对其不生效,测试结论失真;一张后加的表忘了 ENABLE ROW LEVEL SECURITY,成了敞门。修复动作三条:全部业务表启用 FORCE ROW LEVEL SECURITY(所有者也受管);应用账号降为普通角色,超级用户仅限运维登录且禁止业务库长连;把"新表是否启用且强制 RLS、策略是否有配套索引"写进建表代码评审清单。RLS 的安全边界完全由例外清单决定——所有者、超级用户、未启用的表,每个例外都是一个需要流程封堵的门。