行级安全(Row-Level Security)
在 PostgreSQL 中,GRANT 解决的是一张表「谁能看、谁能改」,粒度是表。但很多业务场景的权限判断无法用表来切分:多租户 SaaS 里,A 公司的用户只能看到 A 公司的订单;员工只能看到自己名下的工单;租户管理员只能改自己租户的数据——这些数据都在同一张表里,区别只在行。
行级安全(Row-Level Security,简称 RLS) 就是为解决这个问题而设计的:它把访问控制从「表」下沉到「行」,让数据库层直接保证"任何用户都不可能查询到不该他看的那一行",而不是把过滤条件的正确性押在每一处应用代码上。
本节讲清 RLS 的核心概念、启用方式与整体运作机制,后续小节分别展开策略的创建、USING 与 WITH CHECK 的差异、绕过机制与实际使用场景。
前置知识
阅读本节前,建议先了解:权限与 GRANT/REVOKE、创建扩展
基础概念
从「表级权限」到「行级安全」
传统的 SQL 权限系统(GRANT)回答的问题是:这个角色能不能对这张表做这种操作?
RLS 回答的问题则是:这个角色能在这张表里看到、修改哪些行?
| 维度 | GRANT(表级 / 列级权限) | RLS(行级安全) |
|---|---|---|
| 控制粒度 | 表、列 | 行 |
| 判断依据 | 角色 | 角色 + 行内容 + 会话变量 |
| 命中时的行为 | 无权限直接报错 | 不匹配的行被静默过滤 |
| 默认立场 | 未授权即拒绝 | 启用后无策略即拒绝 |
| 典型用途 | 区分只读 / 读写角色 | 多租户隔离、按属主可见 |
两者不是替代关系,而是叠加关系:一个查询要成功,既要通过 GRANT 的表级权限检查,也要通过 RLS 的行级策略检查。
一次查询经过 RLS 的完整流程
客户端查询
│
▼
┌────────────────────────────────┐
│ 1. 解析 / 重写 │
│ 策略表达式被追加为「安全屏障」│
│ (security barrier)条件 │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ 2. 表级 / 列级权限检查(GRANT) │
│ 不通过 → 报错 │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ 3. 行级策略过滤(RLS) │
│ RESTRICTIVE AND │
│ (PERMISSIVE OR PERMISSIVE …)│
│ 不匹配的行被丢弃 │
└────────────────┬───────────────┘
▼
返回给客户端的行集合关键点:第 3 步的过滤发生在用户查询条件之前。官方文档明确说明,系统会先施加策略过滤条件,再处理用户查询里的 WHERE,目的是防止用户自定义函数在策略生效前接触到受保护的数据(LEAKPROOF 函数是唯一例外,它们可以提前求值)。
RLS 的三个开关
RLS 不是一个单一功能,而是由三层开关共同决定的:
| 层级 | 开关 | 作用 |
|---|---|---|
| 表级 | ALTER TABLE … ENABLE / DISABLE ROW LEVEL SECURITY | 这张表是否启用 RLS |
| 表级 | ALTER TABLE … FORCE / NO FORCE ROW LEVEL SECURITY | 表属主是否也受约束 |
| 策略级 | CREATE POLICY … AS PERMISSIVE / RESTRICTIVE | 单条策略的叠加方式 |
| 会话级 | SET row_security = on / off | 遇到策略过滤时是执行还是报错 |
只有「表级启用 + 存在通过组合的策略」,用户才能看到数据。
关键术语
| 术语 | 含义 |
|---|---|
| 策略(Policy) | 一条命名的行级访问规则,绑定到某张表 |
| 安全策略表达式 | 返回布尔值的 SQL 条件表达式,不能含聚合或窗口函数 |
USING | 判断已有行是否可见 / 可被修改 |
WITH CHECK | 判断新行(INSERT / UPDATE 结果)是否允许写入 |
| PERMISSIVE | 宽松策略,多条之间用 OR 组合(默认) |
| RESTRICTIVE | 限制策略,多条之间用 AND 组合 |
| 默认拒绝(default deny) | 启用了 RLS 但没有任何适用策略时的立场 |
| 绕过(Bypass) | 超级用户、BYPASSRLS 角色、默认情况下的表属主不受 RLS 约束 |
RLS 适合与不适合的场景
适合:
- 多租户(multi-tenant)SaaS,租户数据同表存储;
- 行归属明确的业务表,如"只能看自己的订单 / 工单 / 消息";
- 需要数据库层兜底的合规要求(如医疗、金融数据隔离);
- 多个应用共享一套库,需要各自严格隔离。
不适合:
- 权限判断依赖复杂外部状态(如实时风控接口)——策略表达式无法调用不可控的外部逻辑;
- 单租户单库、权限完全由应用中间件控制的简单系统;
- 高频写入且策略表达式会回查其他表的场景,需要评估性能代价。
语法与用法
启用 / 禁用 RLS
-- 启用 RLS:此后必须由策略放行,行才可见
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 关闭 RLS:策略仍然存在,但被忽略
ALTER TABLE orders DISABLE ROW LEVEL SECURITY;ENABLE 只是打开开关,它本身不放行任何行。如果表上一条策略都没有,结果是所有行都不可见、不可修改(默认拒绝)。DISABLE 不会删除已定义的策略,重新 ENABLE 后它们立刻恢复生效。
让表属主也受约束
-- 表属主默认绕过 RLS;FORCE 后属主也要过策略
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
-- 恢复属主绕过(默认状态)
ALTER TABLE orders NO FORCE ROW LEVEL SECURITY;这一条在生产环境极其重要:很多"策略写对了但没生效"的事故,根因就是应用连的账号恰好是表的属主。
创建策略
CREATE POLICY name ON table_name
[ AS { PERMISSIVE | RESTRICTIVE } ]
[ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
[ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ]
[ USING ( using_expression ) ]
[ WITH CHECK ( check_expression ) ]最小可用形式是「策略名 + 表名 + USING」:
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);创建策略的权限属于表属主:只有表属主才能为它增删改策略。
修改与删除策略
-- 改名
ALTER POLICY tenant_isolation ON orders RENAME TO orders_tenant_isolation;
-- 只改角色列表与表达式
ALTER POLICY tenant_isolation ON orders
TO r_app, r_report
USING (tenant_id = current_setting('app.tenant_id')::int);
-- 删除
DROP POLICY tenant_isolation ON orders;
-- 删除表上的全部策略
DROP POLICY IF EXISTS tenant_isolation ON orders CASCADE;ALTER POLICY 的能力有明确边界:只能修改适用角色列表和 USING / WITH CHECK 表达式。想改命令类型(FOR)或宽松 / 限制属性(AS),必须 DROP 后重建。
查看策略
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'orders';输出示例:
schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check
------------+-----------+--------------------+------------+---------+--------+----------------------------------+------------
public | orders | tenant_isolation | PERMISSIVE | {public}| ALL | (tenant_id = (current_setting('app.tenant_id'::text))::integer) |
public | orders | admin_full_access | PERMISSIVE | {admin} | ALL | true | true
(2 rows)pg_policies 是排查 RLS 问题最常用的视图,字段含义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
schemaname | name | 策略所在表的 schema |
tablename | name | 策略所在表名 |
policyname | name | 策略名 |
permissive | text | PERMISSIVE 或 RESTRICTIVE |
roles | name[] | 策略适用的角色列表 |
cmd | text | 适用命令类型 |
qual | text | 追加为安全屏障条件的表达式(即 USING) |
with_check | text | WITH CHECK 表达式,未定义时为 NULL |
会话参数 row_security
-- 默认值
SHOW row_security;
-- 关闭:一旦查询结果会被策略过滤,直接报错,而不是静默返回空集
SET row_security = off;这个参数的取值只有 on 和 off,没有 force。它的语义容易被误解:
row_security = on(默认):正常执行,策略不适用的行被静默过滤;row_security = off:不绕过 RLS。如果某个查询的结果会被策略过滤掉,直接抛错,提示存在 RLS 过滤。
它的用途是"做备份、做数据校验时,宁可报错也不要在不知情的情况下漏掉行"。
详细说明
默认拒绝:没有策略就是没有数据
启用 RLS 后,只要表上没有适用的(命令类型匹配 + 角色匹配)策略,任何访问都返回空:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 此时表上没有任何策略
SELECT count(*) FROM orders; count
-------
0
(1 row)注意这里是静默返回 0,而不是报"权限不足"。这是 RLS 最反直觉的地方,也是新手最容易误判为"数据丢了"的地方。设计上这是有意的:报错本身会泄露"表里有数据"这一信息。
这条规则有一个例外:表属主默认绕过 RLS,所以用属主账号查同一张表会看到全部行。这才导致了"属主能查、应用账号查不到"的典型现象。
表属主、超级用户与 BYPASSRLS
RLS 的绕过规则必须记牢,它是排障的第一顺位怀疑对象:
| 身份 | 默认是否绕过 RLS | 能否被约束 |
|---|---|---|
| 超级用户(superuser) | 始终绕过 | 不能 |
具有 BYPASSRLS 属性的角色 | 始终绕过 | 不能 |
| 表属主 | 绕过 | 可以,用 FORCE ROW LEVEL SECURITY |
| 普通角色 | 不绕过 | — |
-- 查看当前有效用户是否具备 BYPASSRLS
SELECT rolname, rolsuper, rolbypassrls
FROM pg_roles
WHERE rolname = current_user; rolname | rolsuper | rolbypassrls
-------------+----------+--------------
app_writer | f | f
(1 row)给应用账号授予 BYPASSRLS 就等于关掉了它身上所有的 RLS 保护,包括 FORCE 的效果。这是非常隐蔽的配置错误:FORCE ROW LEVEL SECURITY 只对表属主有效,对超级用户和 BYPASSRLS 角色完全无效。
四种命令 × 两种表达式:搭配矩阵
USING 和 WITH CHECK 不是任意策略都能同时拥有的,必须按命令类型对号入座:
FOR 命令 | 是否有「已有行」 | 是否有「新行」 | 允许 USING | 允许 WITH CHECK |
|---|---|---|---|---|
ALL | 有 | 有 | ✅ | ✅ |
SELECT | 有 | 无 | ✅ | ❌ 不允许 |
INSERT | 无 | 有 | ❌ 不允许 | ✅ |
UPDATE | 有 | 有 | ✅ | ✅ |
DELETE | 有 | 无 | ✅ | ❌ 不允许 |
原因很直白:
INSERT不读取已有行,所以给它写USING没有意义,语法上直接拒绝;SELECT/DELETE不产生新行,所以没有WITH CHECK可言;ALL与UPDATE同时涉及已有行与新行,两者都接受,且若未定义WITH CHECK,则USING会同时兼任新行校验。
-- 合法:INSERT 策略只带 WITH CHECK
CREATE POLICY p_insert ON orders FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);
-- 错误!INSERT 策略不能有 USING
CREATE POLICY p_bad ON orders FOR INSERT
USING (tenant_id = 1);ERROR: "USING" not allowed for INSERT policyPERMISSIVE 与 RESTRICTIVE:OR 与 AND
多条同类型策略的叠加规则是 RLS 的核心:
最终放行条件 =
RESTRICTIVE_1
AND RESTRICTIVE_2
AND ( PERMISSIVE_1 OR PERMISSIVE_2 OR … )三条硬规则:
- 默认是
PERMISSIVE,多条之间OR; RESTRICTIVE之间AND,即每一条都必须通过;- 至少要有一条
PERMISSIVE。如果只有RESTRICTIVE,最终括号里为空,结果按"拒绝"处理。
-- 宽松:管理员看全部,普通用户看自己租户
CREATE POLICY admin_all ON orders TO admin_role USING (true);
CREATE POLICY tenant_only ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);
-- 限制:无论谁,都必须通过「非软删除」这条
CREATE POLICY not_deleted ON orders AS RESTRICTIVE
USING (is_deleted = false);以上三条叠加后:is_deleted = false AND (true OR tenant_id = ...)。管理员看得到全部未删除行,普通用户只看到本租户未删除行。
注意:不同类型的命令策略之间用
AND组合。例如执行UPDATE时,既有SELECT策略(因为UPDATE需要先读到行),又有UPDATE策略,两者都必须通过。
策略表达式以调用者权限运行
策略表达式会被直接追加进用户查询,因此它以运行查询的用户的权限执行:
CREATE POLICY dept_scope ON employees
USING (dept_id IN (SELECT dept_id FROM my_depts));这里 my_depts 表如果对调用者没有 SELECT 权限,用户查 employees 就会直接收到 permission denied for table my_depts——错误信息指向的是一张他没听说过的表,排查时很容易困惑。
如果确实需要用调用者无权访问的数据做判断,标准做法是把逻辑封进 SECURITY DEFINER 函数:
CREATE FUNCTION current_user_depts() RETURNS SETOF int
LANGUAGE sql SECURITY DEFINER STABLE AS $$
SELECT dept_id FROM my_depts WHERE owner = current_user
$$;引用完整性检查不受 RLS 约束
官方文档明确指出:唯一约束、主键、外键等引用完整性检查总是绕过 RLS,以保证数据完整性。这带来两个需要警惕的后果:
- 往带唯一约束的列插入重复值时,报错与否会暴露"该值已存在"这一信息;
- 往引用其他表的外键列插入值时,插入成功与否会暴露"被引用表中存在该值"。
这被称为隐蔽信道(covert channel) 泄漏。缓解方式:
-- 不推荐:把业务含义的键直接暴露为唯一键
CREATE TABLE tenants (code text PRIMARY KEY, ...);
-- 推荐:用无业务含义的代理键,避免外部可探测
CREATE TABLE tenants (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ...);视图与 RLS:security_invoker
视图默认以视图属主的权限和策略访问底层表。也就是说,即使用户被 RLS 限制,只要视图属主能看全部数据,用户通过视图也能看到全部——这是一个非常常见的安全漏洞。
-- 危险:默认行为下,视图属主是 admin,用户绕过 RLS 看到全部
CREATE VIEW v_orders AS SELECT * FROM orders;
-- 安全:PG15+ 让视图以「调用者」的权限与策略执行
CREATE VIEW v_orders WITH (security_invoker = true) AS
SELECT * FROM orders;security_invoker 是 PostgreSQL 15 引入的视图选项,PG18 完全可用。任何面向多租户暴露的视图,都应该显式打开它。
与列级权限的配合
RLS 与列级 GRANT 是正交的,可以叠加出很细的权限模型。官方示例中就演示了这种组合:普通用户只有部分列的 SELECT 权限、部分列的 UPDATE 权限,再用 RLS 限制只能操作自己的行。
-- 列级权限:只能读非敏感列
GRANT SELECT (user_name, uid, real_name, home_dir, shell) ON passwd TO PUBLIC;
-- 列级权限:只能改这几列
GRANT UPDATE (pwhash, real_name, shell) ON passwd TO PUBLIC;此时"只能改自己那行"由 RLS 保证,"只能改某几列"由列级 GRANT 保证,两者互不替代。
RLS 的性能特征
策略表达式会被追加到每次查询上,因此它的代价直接影响所有访问:
- 只引用当前行(如
tenant_id = current_setting(...)::int)是最快的形式,能直接走索引; - 回查其他表或调用函数会引入额外扫描,且可能因快照不一致产生竞态(见下);
- 追加的条件是安全屏障,会阻止某些优化(例如条件下推、连接顺序重排),复杂策略下执行计划可能明显变差;
- 策略里用
current_setting()读取会话变量,是STABLE的,不会每行重复求值。
-- 推荐:单行判断,可用索引
CREATE POLICY p ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);
-- 谨慎:回查其他表,可能引入竞态与额外扫描
CREATE POLICY p2 ON info FOR SELECT
USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));关于竞态:如果策略子查询在 READ COMMITTED 下读到的是查询开始时的快照,而另一个事务正在修改被引用的权限表,就可能出现"用旧的权限等级放行了新数据"的窗口。官方建议的缓解手段包括:在策略子查询里使用 SELECT ... FOR SHARE、在更新被引用表时加 ACCESS EXCLUSIVE 锁,或干脆避免在策略中回查其他表。
启用与禁用的权限边界
启用 / 禁用 RLS、增删改策略,都是表属主专属的权限。 即使你是超级用户之外的"表的管理者",只要不是属主,也没有权限改动:
-- 非属主执行会失败
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;ERROR: must be owner of table orders权限本身通过 GRANT 授予角色,RLS 的开关与策略则通过属主身份控制,两套机制各管一段。
COPY 与 RLS 的限制
这一点在 PG18 中必须特别注意:
COPY ... FROM不支持 RLS 表。官方文档明确说明当前COPY FROM不适用于启用了行级安全的表;COPY table TO会正常应用SELECT策略,导出的是当前用户可见的行。
-- 对被 RLS 保护的表,这会失败
COPY orders FROM '/data/orders.csv' WITH (FORMAT csv);ERROR: COPY FROM not supported with row-level security
HINT: Use INSERT statements instead.因此批量导入受 RLS 保护的表时,必须改用等价的 INSERT:
-- 替代方案
INSERT INTO orders (id, tenant_id, amount)
SELECT id, tenant_id, amount FROM staging_orders;MERGE 如何应用策略
MERGE 没有独立的策略类型。执行 MERGE 时,会按它实际执行的动作,分别应用对应的 SELECT / INSERT / UPDATE / DELETE 策略:MERGE ... THEN INSERT 走 INSERT 策略,THEN UPDATE 走 UPDATE 策略,依此类推。设计策略时不能只想着四条基础命令,还要考虑 MERGE 会组合触发它们。
一个完整的决策清单
判断"某用户对某表能做什么"时,按顺序走完:
- 用户是否有表级 / 列级的
GRANT权限?没有 → 报错; - 用户是否超级用户 /
BYPASSRLS?是 → 不受 RLS 约束,结束; - 用户是否是表属主?是且未
FORCE→ 不受 RLS 约束,结束; - 表是否启用了 RLS?否 → 不受 RLS 约束,结束;
- 是否有匹配当前命令类型和当前角色的
PERMISSIVE策略?没有 → 全拒绝; - 计算
RESTRICTIVE AND (PERMISSIVE OR …),得出最终可见行集合。
实战示例
示例一:多租户订单表的最小可用配置
-- 1. 建表:租户列是隔离的基石
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id int NOT NULL,
user_id int NOT NULL,
amount numeric(12,2) NOT NULL,
is_deleted boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_tenant ON orders (tenant_id);
-- 2. 应用账号与租户管理账号
CREATE ROLE app_user LOGIN PASSWORD 'change-me';
CREATE ROLE tenant_admin LOGIN PASSWORD 'change-me';
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user, tenant_admin;
-- 3. 启用 RLS,并让属主也受约束
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
-- 4. 本租户可见(应用账号)
CREATE POLICY p_tenant ON orders TO app_user
USING (tenant_id = current_setting('app.tenant_id')::int)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);
-- 5. 租户管理员可读全租户,但不可跨租户写
CREATE POLICY p_admin_read ON orders TO tenant_admin FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::int);
-- 6. 全局限制:软删除的行谁都别想看到
CREATE POLICY p_not_deleted ON orders AS RESTRICTIVE
USING (is_deleted = false);按租户上下文查询:
SET app.tenant_id = '42';
SELECT id, amount FROM orders WHERE tenant_id = 42 LIMIT 3; id | amount
----+---------
7 | 120.00
9 | 88.50
15 | 1024.00
(3 rows)切换租户上下文后,同一账号看到的是完全不同的行集合:
SET app.tenant_id = '99';
SELECT count(*) FROM orders; count
-------
17
(1 row)示例二:验证「默认拒绝」与「属主绕过」
-- 建一张实验表
CREATE TABLE demo_rls (id int, owner_name text);
INSERT INTO demo_rls VALUES (1, 'alice'), (2, 'bob');
CREATE ROLE demo_user LOGIN;
GRANT SELECT ON demo_rls TO demo_user;
ALTER TABLE demo_rls ENABLE ROW LEVEL SECURITY;此时表上没有任何策略:
SET ROLE demo_user;
SELECT * FROM demo_rls; id | owner_name
----+------------
(0 rows)静默返回空集,不报错——这就是默认拒绝。切回属主(不带 FORCE):
RESET ROLE; -- 回到表属主
SELECT * FROM demo_rls; id | owner_name
----+------------
1 | alice
2 | bob
(2 rows)属主绕过 RLS,看到了全部行。加上 FORCE 之后,属主也会被同样约束:
ALTER TABLE demo_rls FORCE ROW LEVEL SECURITY;
SELECT * FROM demo_rls; -- 属主身份,但已 FORCE id | owner_name
----+------------
(0 rows)这条对比是理解 RLS 行为最直观的实验,建议在测试库亲手跑一遍。
注意事项
别忘了 ENABLE
只 CREATE POLICY 而不 ENABLE,策略完全不生效,且不会有任何提示:
CREATE POLICY p ON orders USING (tenant_id = 1); -- 不生效
ALTER TABLE orders ENABLE ROW LEVEL SECURITY; -- 必须补上这句属主绕过是最常见的"策略失效"原因
应用连接账号如果恰好是表属主,所有策略都会形同虚设:
-- 检查应用账号是不是属主
SELECT relname, pg_get_userbyid(relowner) AS owner
FROM pg_class WHERE relname = 'orders';
-- 生产环境强烈建议
ALTER TABLE orders FORCE ROW LEVEL SECURITY;只建 RESTRICTIVE 策略等于没有数据
没有 PERMISSIVE 放行时,RESTRICTIVE 无从"限制",结果是全拒绝:
-- 错误用法:只有限制策略,谁都看不到
CREATE POLICY only_restrictive ON orders AS RESTRICTIVE USING (is_deleted = false);不要给应用账号 BYPASSRLS
它会让 FORCE 也失效,是隐蔽的安全缺口:
-- 不推荐
ALTER ROLE app_user BYPASSRLS;
-- 需要"绕过"时,用专门的、受审计的账号,而不是应用账号INSERT 不能写 USING,SELECT/DELETE 不能写 WITH CHECK
这是语法级限制,写错会直接建不出来:
-- 错误!
CREATE POLICY p ON orders FOR INSERT USING (tenant_id = 1);row_security = off 不是绕过开关
它的作用是"宁可报错也不静默漏行",不是"忽略策略":
-- 误解:以为这是绕过
SET row_security = off;
SELECT * FROM orders; -- 若结果会被过滤,直接报错策略里回查其他表要小心权限与竞态
调用者需要对被引用表有权限,同时注意快照一致性:
-- 若调用者无 my_depts 的 SELECT 权限,会报 permission denied
CREATE POLICY p ON employees USING (dept_id IN (SELECT dept_id FROM my_depts));暴露给多租户的视图要加 security_invoker
否则视图属主的权限会让用户越过 RLS:
-- 不推荐(默认使用视图属主权限)
CREATE VIEW v_orders AS SELECT * FROM orders;
-- 推荐
CREATE VIEW v_orders WITH (security_invoker = true) AS SELECT * FROM orders;批量导入不能用 COPY FROM
PG18 中 COPY FROM 不支持 RLS 表,必须改用 INSERT ... SELECT:
-- 错误!对 RLS 表会失败
COPY orders FROM '/tmp/orders.csv';策略是"静默过滤",排障时先看行数
查询不报错但结果为空,优先怀疑 RLS:
-- 先确认策略与开关
SELECT * FROM pg_policies WHERE tablename = 'orders';
SELECT relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'orders';最佳实践
强制表属主受约束
生产表一律 FORCE ROW LEVEL SECURITY,消除"属主视角与用户视角不一致"这类陷阱:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;隔离维度用显式列并建索引
RLS 的核心是"按某个维度隔离",这个维度列必须存在且被索引,否则每次查询都要全表扫描:
ALTER TABLE orders ADD COLUMN tenant_id int NOT NULL;
CREATE INDEX idx_orders_tenant ON orders (tenant_id);会话上下文用自定义 GUC 传递
不要在应用里手动拼 WHERE tenant_id = ...,用 SET LOCAL 在事务内传上下文,让 RLS 做唯一的事实来源:
BEGIN;
SET LOCAL app.tenant_id = '42';
SELECT * FROM orders; -- 应用代码不需要写租户过滤
COMMIT;用 SET LOCAL 而不是 SET
SET LOCAL 随事务结束自动失效,避免连接池复用连接时上下文串租户:
-- 推荐:事务级,自动复位
SET LOCAL app.tenant_id = '42';
-- 谨慎:会话级,连接归还池前必须手动 RESET
SET app.tenant_id = '42';配一个明确的"全权"策略而不是依赖绕过
让管理员通过一条 PERMISSIVE 策略看到全部,比依赖 BYPASSRLS 更可审计:
CREATE POLICY admin_all ON orders TO admin_role
USING (true) WITH CHECK (true);软删除用 RESTRICTIVE 统一兜底
把"不可见"这类全局约束写成 RESTRICTIVE,避免在每条业务策略里重复:
CREATE POLICY p_not_deleted ON orders AS RESTRICTIVE USING (is_deleted = false);策略表达式保持简单、可用索引
优先只引用当前行的列和 STABLE 的会话变量,把复杂判断挪到写入侧或物化列:
-- 好:单行判断 + 索引列
USING (tenant_id = current_setting('app.tenant_id')::int)为策略写测试用例
RLS 是安全边界,必须像对待密码校验一样写自动化测试,覆盖"他人数据不可见 / 不可写":
-- 断言:以 bob 身份查不到 alice 的行
SET LOCAL app.user_id = '2';
SELECT count(*) FROM orders WHERE user_id = 1; -- 期望 0定期审计策略清单
用 pg_policies 与 pg_class 做例行检查,防止有人偷偷 DISABLE 或删策略:
SELECT tablename, policyname, permissive, roles, cmd
FROM pg_policies ORDER BY tablename, policyname;
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relnamespace = 'public'::regnamespace AND relkind = 'r';上线前用 EXPLAIN 复核性能
确认策略条件没有破坏索引使用,尤其是策略含子查询时:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE tenant_id = 42;迁移数据时临时使用受审计的特权账号
逻辑备份 / 校验需要看到全部行,此时用专门的、事后撤销权限的账号,而不是给应用账号 BYPASSRLS:
-- 用完即撤销
GRANT SELECT ON orders TO audit_reader;
-- ...
REVOKE SELECT ON orders FROM audit_reader;下一节
继续学习:创建与管理策略