Skip to content

行级安全(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 的完整流程 ​

text
                     客户端查询
                         │
                         ▼
        ┌────────────────────────────────┐
        │  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 ​

sql
-- 启用 RLS:此后必须由策略放行,行才可见
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 关闭 RLS:策略仍然存在,但被忽略
ALTER TABLE orders DISABLE ROW LEVEL SECURITY;

ENABLE 只是打开开关,它本身不放行任何行。如果表上一条策略都没有,结果是所有行都不可见、不可修改(默认拒绝)。DISABLE 不会删除已定义的策略,重新 ENABLE 后它们立刻恢复生效。

让表属主也受约束 ​

sql
-- 表属主默认绕过 RLS;FORCE 后属主也要过策略
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

-- 恢复属主绕过(默认状态)
ALTER TABLE orders NO FORCE ROW LEVEL SECURITY;

这一条在生产环境极其重要:很多"策略写对了但没生效"的事故,根因就是应用连的账号恰好是表的属主。

创建策略 ​

sql
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」:

sql
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::int);

创建策略的权限属于表属主:只有表属主才能为它增删改策略。

修改与删除策略 ​

sql
-- 改名
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 后重建。

查看策略 ​

sql
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'orders';

输出示例:

text
 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 问题最常用的视图,字段含义如下:

字段类型说明
schemanamename策略所在表的 schema
tablenamename策略所在表名
policynamename策略名
permissivetextPERMISSIVE 或 RESTRICTIVE
rolesname[]策略适用的角色列表
cmdtext适用命令类型
qualtext追加为安全屏障条件的表达式(即 USING)
with_checktextWITH CHECK 表达式,未定义时为 NULL

会话参数 row_security ​

sql
-- 默认值
SHOW row_security;

-- 关闭:一旦查询结果会被策略过滤,直接报错,而不是静默返回空集
SET row_security = off;

这个参数的取值只有 on 和 off,没有 force。它的语义容易被误解:

  • row_security = on(默认):正常执行,策略不适用的行被静默过滤;
  • row_security = off:不绕过 RLS。如果某个查询的结果会被策略过滤掉,直接抛错,提示存在 RLS 过滤。

它的用途是"做备份、做数据校验时,宁可报错也不要在不知情的情况下漏掉行"。

详细说明 ​

默认拒绝:没有策略就是没有数据 ​

启用 RLS 后,只要表上没有适用的(命令类型匹配 + 角色匹配)策略,任何访问都返回空:

sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 此时表上没有任何策略
SELECT count(*) FROM orders;
text
 count
-------
     0
(1 row)

注意这里是静默返回 0,而不是报"权限不足"。这是 RLS 最反直觉的地方,也是新手最容易误判为"数据丢了"的地方。设计上这是有意的:报错本身会泄露"表里有数据"这一信息。

这条规则有一个例外:表属主默认绕过 RLS,所以用属主账号查同一张表会看到全部行。这才导致了"属主能查、应用账号查不到"的典型现象。

表属主、超级用户与 BYPASSRLS ​

RLS 的绕过规则必须记牢,它是排障的第一顺位怀疑对象:

身份默认是否绕过 RLS能否被约束
超级用户(superuser)始终绕过不能
具有 BYPASSRLS 属性的角色始终绕过不能
表属主绕过可以,用 FORCE ROW LEVEL SECURITY
普通角色不绕过—
sql
-- 查看当前有效用户是否具备 BYPASSRLS
SELECT rolname, rolsuper, rolbypassrls
FROM pg_roles
WHERE rolname = current_user;
text
   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 会同时兼任新行校验。
sql
-- 合法: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);
text
ERROR:  "USING" not allowed for INSERT policy

PERMISSIVE 与 RESTRICTIVE:OR 与 AND ​

多条同类型策略的叠加规则是 RLS 的核心:

text
最终放行条件 =
    RESTRICTIVE_1
    AND RESTRICTIVE_2
    AND ( PERMISSIVE_1 OR PERMISSIVE_2 OR … )

三条硬规则:

  1. 默认是 PERMISSIVE,多条之间 OR;
  2. RESTRICTIVE 之间 AND,即每一条都必须通过;
  3. 至少要有一条 PERMISSIVE。如果只有 RESTRICTIVE,最终括号里为空,结果按"拒绝"处理。
sql
-- 宽松:管理员看全部,普通用户看自己租户
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 策略,两者都必须通过。

策略表达式以调用者权限运行 ​

策略表达式会被直接追加进用户查询,因此它以运行查询的用户的权限执行:

sql
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 函数:

sql
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) 泄漏。缓解方式:

sql
-- 不推荐:把业务含义的键直接暴露为唯一键
CREATE TABLE tenants (code text PRIMARY KEY, ...);

-- 推荐:用无业务含义的代理键,避免外部可探测
CREATE TABLE tenants (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ...);

视图与 RLS:security_invoker ​

视图默认以视图属主的权限和策略访问底层表。也就是说,即使用户被 RLS 限制,只要视图属主能看全部数据,用户通过视图也能看到全部——这是一个非常常见的安全漏洞。

sql
-- 危险:默认行为下,视图属主是 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 限制只能操作自己的行。

sql
-- 列级权限:只能读非敏感列
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 的,不会每行重复求值。
sql
-- 推荐:单行判断,可用索引
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、增删改策略,都是表属主专属的权限。 即使你是超级用户之外的"表的管理者",只要不是属主,也没有权限改动:

sql
-- 非属主执行会失败
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
text
ERROR:  must be owner of table orders

权限本身通过 GRANT 授予角色,RLS 的开关与策略则通过属主身份控制,两套机制各管一段。

COPY 与 RLS 的限制 ​

这一点在 PG18 中必须特别注意:

  • COPY ... FROM 不支持 RLS 表。官方文档明确说明当前 COPY FROM 不适用于启用了行级安全的表;
  • COPY table TO 会正常应用 SELECT 策略,导出的是当前用户可见的行。
sql
-- 对被 RLS 保护的表,这会失败
COPY orders FROM '/data/orders.csv' WITH (FORMAT csv);
text
ERROR:  COPY FROM not supported with row-level security
HINT:  Use INSERT statements instead.

因此批量导入受 RLS 保护的表时,必须改用等价的 INSERT:

sql
-- 替代方案
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 会组合触发它们。

一个完整的决策清单 ​

判断"某用户对某表能做什么"时,按顺序走完:

  1. 用户是否有表级 / 列级的 GRANT 权限?没有 → 报错;
  2. 用户是否超级用户 / BYPASSRLS?是 → 不受 RLS 约束,结束;
  3. 用户是否是表属主?是且未 FORCE → 不受 RLS 约束,结束;
  4. 表是否启用了 RLS?否 → 不受 RLS 约束,结束;
  5. 是否有匹配当前命令类型和当前角色的 PERMISSIVE 策略?没有 → 全拒绝;
  6. 计算 RESTRICTIVE AND (PERMISSIVE OR …),得出最终可见行集合。

实战示例 ​

示例一:多租户订单表的最小可用配置 ​

sql
-- 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);

按租户上下文查询:

sql
SET app.tenant_id = '42';
SELECT id, amount FROM orders WHERE tenant_id = 42 LIMIT 3;
text
 id | amount
----+---------
  7 |  120.00
  9 |   88.50
 15 | 1024.00
(3 rows)

切换租户上下文后,同一账号看到的是完全不同的行集合:

sql
SET app.tenant_id = '99';
SELECT count(*) FROM orders;
text
 count
-------
    17
(1 row)

示例二:验证「默认拒绝」与「属主绕过」 ​

sql
-- 建一张实验表
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;

此时表上没有任何策略:

sql
SET ROLE demo_user;
SELECT * FROM demo_rls;
text
 id | owner_name
----+------------
(0 rows)

静默返回空集,不报错——这就是默认拒绝。切回属主(不带 FORCE):

sql
RESET ROLE;   -- 回到表属主
SELECT * FROM demo_rls;
text
 id | owner_name
----+------------
  1 | alice
  2 | bob
(2 rows)

属主绕过 RLS,看到了全部行。加上 FORCE 之后,属主也会被同样约束:

sql
ALTER TABLE demo_rls FORCE ROW LEVEL SECURITY;
SELECT * FROM demo_rls;   -- 属主身份,但已 FORCE
text
 id | owner_name
----+------------
(0 rows)

这条对比是理解 RLS 行为最直观的实验,建议在测试库亲手跑一遍。

注意事项 ​

别忘了 ENABLE

只 CREATE POLICY 而不 ENABLE,策略完全不生效,且不会有任何提示:

sql
CREATE POLICY p ON orders USING (tenant_id = 1);   -- 不生效
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;      -- 必须补上这句

属主绕过是最常见的"策略失效"原因

应用连接账号如果恰好是表属主,所有策略都会形同虚设:

sql
-- 检查应用账号是不是属主
SELECT relname, pg_get_userbyid(relowner) AS owner
FROM pg_class WHERE relname = 'orders';

-- 生产环境强烈建议
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

只建 RESTRICTIVE 策略等于没有数据

没有 PERMISSIVE 放行时,RESTRICTIVE 无从"限制",结果是全拒绝:

sql
-- 错误用法:只有限制策略,谁都看不到
CREATE POLICY only_restrictive ON orders AS RESTRICTIVE USING (is_deleted = false);

不要给应用账号 BYPASSRLS

它会让 FORCE 也失效,是隐蔽的安全缺口:

sql
-- 不推荐
ALTER ROLE app_user BYPASSRLS;

-- 需要"绕过"时,用专门的、受审计的账号,而不是应用账号

INSERT 不能写 USING,SELECT/DELETE 不能写 WITH CHECK

这是语法级限制,写错会直接建不出来:

sql
-- 错误!
CREATE POLICY p ON orders FOR INSERT USING (tenant_id = 1);

row_security = off 不是绕过开关

它的作用是"宁可报错也不静默漏行",不是"忽略策略":

sql
-- 误解:以为这是绕过
SET row_security = off;
SELECT * FROM orders;   -- 若结果会被过滤,直接报错

策略里回查其他表要小心权限与竞态

调用者需要对被引用表有权限,同时注意快照一致性:

sql
-- 若调用者无 my_depts 的 SELECT 权限,会报 permission denied
CREATE POLICY p ON employees USING (dept_id IN (SELECT dept_id FROM my_depts));

暴露给多租户的视图要加 security_invoker

否则视图属主的权限会让用户越过 RLS:

sql
-- 不推荐(默认使用视图属主权限)
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:

sql
-- 错误!对 RLS 表会失败
COPY orders FROM '/tmp/orders.csv';

策略是"静默过滤",排障时先看行数

查询不报错但结果为空,优先怀疑 RLS:

sql
-- 先确认策略与开关
SELECT * FROM pg_policies WHERE tablename = 'orders';
SELECT relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'orders';

最佳实践 ​

强制表属主受约束

生产表一律 FORCE ROW LEVEL SECURITY,消除"属主视角与用户视角不一致"这类陷阱:

sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

隔离维度用显式列并建索引

RLS 的核心是"按某个维度隔离",这个维度列必须存在且被索引,否则每次查询都要全表扫描:

sql
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 做唯一的事实来源:

sql
BEGIN;
SET LOCAL app.tenant_id = '42';
SELECT * FROM orders;   -- 应用代码不需要写租户过滤
COMMIT;

用 SET LOCAL 而不是 SET

SET LOCAL 随事务结束自动失效,避免连接池复用连接时上下文串租户:

sql
-- 推荐:事务级,自动复位
SET LOCAL app.tenant_id = '42';
-- 谨慎:会话级,连接归还池前必须手动 RESET
SET app.tenant_id = '42';

配一个明确的"全权"策略而不是依赖绕过

让管理员通过一条 PERMISSIVE 策略看到全部,比依赖 BYPASSRLS 更可审计:

sql
CREATE POLICY admin_all ON orders TO admin_role
    USING (true) WITH CHECK (true);

软删除用 RESTRICTIVE 统一兜底

把"不可见"这类全局约束写成 RESTRICTIVE,避免在每条业务策略里重复:

sql
CREATE POLICY p_not_deleted ON orders AS RESTRICTIVE USING (is_deleted = false);

策略表达式保持简单、可用索引

优先只引用当前行的列和 STABLE 的会话变量,把复杂判断挪到写入侧或物化列:

sql
-- 好:单行判断 + 索引列
USING (tenant_id = current_setting('app.tenant_id')::int)

为策略写测试用例

RLS 是安全边界,必须像对待密码校验一样写自动化测试,覆盖"他人数据不可见 / 不可写":

sql
-- 断言:以 bob 身份查不到 alice 的行
SET LOCAL app.user_id = '2';
SELECT count(*) FROM orders WHERE user_id = 1;   -- 期望 0

定期审计策略清单

用 pg_policies 与 pg_class 做例行检查,防止有人偷偷 DISABLE 或删策略:

sql
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 复核性能

确认策略条件没有破坏索引使用,尤其是策略含子查询时:

sql
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE tenant_id = 42;

迁移数据时临时使用受审计的特权账号

逻辑备份 / 校验需要看到全部行,此时用专门的、事后撤销权限的账号,而不是给应用账号 BYPASSRLS:

sql
-- 用完即撤销
GRANT SELECT ON orders TO audit_reader;
-- ...
REVOKE SELECT ON orders FROM audit_reader;

下一节

继续学习:创建与管理策略

参考链接 ​