Skip to content

创建与管理策略 ​

上一节讲清了 RLS 的整体机制,本节聚焦最日常的工作:如何把一条策略准确写出来。

CREATE POLICY 看着只有六七个可选子句,但要写得对、写得安全,必须搞清楚三件事:策略的属性(AS 与 FOR)、策略的作用对象(TO)、以及策略的判断条件(USING / WITH CHECK)。子句顺序有讲究,命令类型与表达式之间有硬性搭配限制,角色列表的展开规则也容易踩坑。

本节把 CREATE POLICY 的完整语法拆开逐项讲解,再用真实场景演示如何把业务需求翻译成策略。

前置知识

阅读本节前,建议先了解:行级安全(RLS)概览

基础概念 ​

一条策略由三部分组成 ​

text
CREATE POLICY 策略名 ON 表名
    ├── ① 属性:AS PERMISSIVE | RESTRICTIVE  +  FOR ALL | SELECT | INSERT | UPDATE | DELETE
    ├── ② 对象:TO 角色列表
    └── ③ 条件:USING (...)  /  WITH CHECK (...)
  • 属性决定"这条策略怎么参与组合、管哪类命令";
  • 对象决定"这条策略对哪些角色生效";
  • 条件决定"哪些行放行"。

三个部分全都有默认值:不写 AS 默认 PERMISSIVE,不写 FOR 默认 ALL,不写 TO 默认 PUBLIC。

语法骨架 ​

text
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 ) ]

子句必须按上述顺序书写;除策略名和 ON table_name 外,其余均可省略。

三种"角色写法"在策略里的语义 ​

TO 子句里的这三个关键字容易被误认为可互换,实际语义完全不同:

写法求值时机含义
role_name策略生效时匹配明确指定一个或多个角色
PUBLIC—所有角色
CURRENT_USER策略生效时展开为创建策略时执行该语句的当前用户
CURRENT_ROLE策略生效时展开为创建策略时的当前角色
SESSION_USER策略生效时展开为创建策略时的会话用户

关键点:CURRENT_USER / CURRENT_ROLE / SESSION_USER 在 TO 子句里是创建时求值并固化为具体角色名,不是在查询时动态匹配"谁是当前用户"。想要"每行只对自己可见",正确做法是在 USING 里写 current_user,而不是指望 TO CURRENT_USER:

sql
-- 正确:每次查询时按执行者动态判断
CREATE POLICY p_own ON messages USING (owner = current_user);

-- 危险:TO CURRENT_USER 会被固化成「创建策略那一刻的用户」
CREATE POLICY p_own_bad ON messages TO CURRENT_USER USING (true);

命令类型与表达式的搭配矩阵 ​

FOR读取已有行产生新行USING 必需性WITH CHECK 必需性
ALL✅✅可省略可省略
SELECT✅—可省略(省略即等价 USING (true))不允许
INSERT—✅不允许可省略
UPDATE✅✅可省略可省略
DELETE✅—可省略不允许

记住三条:

  1. INSERT 没有"已有行",所以不能写 USING;
  2. SELECT / DELETE 不产生新行,所以不能写 WITH CHECK;
  3. ALL 和 UPDATE 两者都能写,若不写 WITH CHECK,则 USING 兼任新行校验。

语法与用法 ​

最小策略 ​

sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

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

只有策略名、表名和一个 USING。等价于 AS PERMISSIVE FOR ALL TO PUBLIC,并且由于没写 WITH CHECK,这条 USING 同时作用于选中行与新写入行。

完整策略 ​

sql
CREATE POLICY p_tenant_rw ON orders
    AS PERMISSIVE
    FOR ALL
    TO app_user, tenant_admin
    USING (tenant_id = current_setting('app.tenant_id')::int)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

六个子句全写上,是排查问题时的推荐写法:所有默认行为都被显式化,读代码的人不需要脑内补默认值。

只读策略 ​

sql
CREATE POLICY p_tenant_read ON orders
    FOR SELECT
    TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int);

SELECT 策略只决定可见行,不限制写入。注意:要想让用户"只能读不能写",还需配合 GRANT(不授予 INSERT / UPDATE / DELETE),RLS 与 GRANT 各管一段。

只写策略 ​

sql
CREATE POLICY p_tenant_insert ON orders
    FOR INSERT
    TO app_user
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

INSERT 策略只校验新行,不能写 USING。若同时希望用户能读到自己的插入结果,还需一条 SELECT 策略或一条 ALL 策略。

更新策略 ​

sql
CREATE POLICY p_tenant_update ON orders
    FOR UPDATE
    TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

UPDATE 是最需要显式写两个表达式的情况:USING 决定"能改哪些行",WITH CHECK 决定"改完之后的行还合法吗"。两者都写可以防止把行"改出"自己的租户。

修改策略 ​

sql
ALTER POLICY name ON table_name RENAME TO new_name

ALTER POLICY name ON table_name
    [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ]
    [ USING ( using_expression ) ]
    [ WITH CHECK ( check_expression ) ]
sql
-- 只改角色列表
ALTER POLICY p_tenant ON orders TO app_user, r_report;

-- 只改表达式(角色列表不变)
ALTER POLICY p_tenant ON orders
    USING (tenant_id = current_setting('app.tenant_id')::int AND is_deleted = false);

ALTER POLICY 有个明确限制:只能改角色列表、USING、WITH CHECK 和策略名。想改 FOR 命令类型或 AS 宽松 / 限制属性,必须删除重建。同时,被省略的子句保持原样不变。

删除策略 ​

sql
-- 单条删除
DROP POLICY p_tenant ON orders;

-- 幂等写法
DROP POLICY IF EXISTS p_tenant ON orders;

-- 一条语句删除多个
DROP POLICY IF EXISTS p_a, p_b ON orders;

查看表上有哪些策略 ​

sql
SELECT policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND tablename = 'orders'
ORDER BY policyname;
text
    policyname    | permissive |    roles     |  cmd   |              qual               |           with_check
------------------+------------+--------------+--------+---------------------------------+--------------------------------
 p_tenant_insert  | PERMISSIVE | {app_user}   | INSERT |                                 | (tenant_id = (current_setting('app.tenant_id'::text))::integer)
 p_tenant_read    | PERMISSIVE | {app_user}   | SELECT | (tenant_id = (current_setting('app.tenant_id'::text))::integer) |
 p_tenant_update  | PERMISSIVE | {app_user}   | UPDATE | (tenant_id = (current_setting('app.tenant_id'::text))::integer) |
 p_not_deleted    | RESTRICTIVE| {public}     | ALL    | (is_deleted = false)            | (is_deleted = false)
(4 rows)

注意 qual 列里 PostgreSQL 会把表达式做一次规范化重写:current_setting('app.tenant_id') 被补成了 current_setting('app.tenant_id'::text),::int 被展开为 ::integer。这是正常的,不是你把语句写错了。

查看表级的 RLS 开关 ​

sql
SELECT relname,
       relrowsecurity,
       relforcerowsecurity,
       pg_get_userbyid(relowner) AS owner
FROM pg_class
WHERE relname = 'orders';
text
 relname | relrowsecurity | relforcerowsecurity |  owner
---------+----------------+---------------------+---------
 orders  | t              | t                   | dba_app
(1 row)

relrowsecurity 对应 ENABLE,relforcerowsecurity 对应 FORCE。

详细说明 ​

省略 USING 会发生什么 ​

如果一条策略既没写 USING 也没写 WITH CHECK,它等价于 USING (true):

sql
-- 这条策略对所有行都放行,对 PUBLIC 生效
CREATE POLICY p_open ON orders FOR SELECT;

这在"我只想给某个角色开个口子"时确实有用,但极容易造成越权。建议任何面向业务表的策略都显式写出 USING,把 USING (true) 留给确实需要全权放行的管理员策略。

sql
-- 明确表达意图:管理员全权
CREATE POLICY p_admin ON orders TO admin_role FOR ALL
    USING (true) WITH CHECK (true);

角色列表的展开与继承 ​

TO 里可以列多个角色,角色继承关系正常生效:

sql
CREATE ROLE r_report;
GRANT r_tenant_reader TO r_report;   -- r_report 继承了 r_tenant_reader

CREATE POLICY p_read ON orders TO r_tenant_reader FOR SELECT
    USING (tenant_id = current_setting('app.tenant_id')::int);

此时以 r_report 身份查询,r_tenant_reader 的策略也适用,因为角色继承规则在策略匹配中同样起作用。

一个易踩的坑是 PUBLIC:默认策略对 PUBLIC 生效,意味着所有角色(包括那些你为了管理用途新建的角色)都会被这条策略约束。如果某个管理角色要通过 PERMISSIVE 策略放行全部,记得它同时也要满足所有 RESTRICTIVE 策略。

FOR SELECT 也会影响 UPDATE / DELETE ​

SELECT 策略不仅管 SELECT,凡是查询"需要读权限"的地方都用它,包括:

  • UPDATE ... WHERE ...:先要读到行,因此 SELECT 策略参与;
  • DELETE ... WHERE ...:同上;
  • INSERT ... RETURNING:返回新行也需要读权限;
  • UPDATE ... RETURNING:需要读旧行与新行。
sql
-- 用户既要能 "看到" 行(SELECT 策略),又要能 "改" 行(UPDATE 策略)
CREATE POLICY p_sel ON orders FOR SELECT TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY p_upd ON orders FOR UPDATE TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

只建 UPDATE 策略而不建 SELECT 策略时,UPDATE 会失败,因为读不到待更新的行。

USING 与 WITH CHECK 各自的作用点 ​

text
                ┌─────────────────────────────────────────┐
   已有行 ──────▶│  USING(这个行我能看到 / 我能操作吗?)    │────▶ 放行
                └─────────────────────────────────────────┘

                ┌─────────────────────────────────────────┐
   新行   ──────▶│  WITH CHECK(这行我能写进去吗?)         │────▶ 写入
                └─────────────────────────────────────────┘

一句话:USING 管"看得见",WITH CHECK 管"写得进"。USING 返回 false 或 NULL 时行被静默过滤;WITH CHECK 返回 false 或 NULL 时直接报错。

表达式的能力与限制 ​

策略表达式必须是返回布尔的 SQL 条件表达式,并且:

  • 不能含聚合函数(count()、sum() 等);
  • 不能含窗口函数(row_number() 等);
  • 可以调用函数,但该函数对调用者必须可见、可执行;
  • 可以引用当前行的任意列;
  • 可以引用 current_user、current_setting() 等会话信息。
sql
-- 合法
CREATE POLICY p1 ON orders USING (amount > 0 AND tenant_id = 1);

-- 错误!策略表达式不能含聚合
CREATE POLICY p2 ON orders USING (amount > avg(amount));
text
ERROR:  aggregate functions are not allowed in policy expressions

想用"平均值"这类派生条件,正确做法是在写入时物化到列上,或存到一个配置表里再引用。

用子查询实现"看别人"的权限 ​

USING 里允许子查询,这打开了更复杂的权限模型。官方经典示例是"按用户的安全等级过滤信息表":

sql
CREATE POLICY fp_s ON information FOR SELECT
    USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));

这行策略表达"我的等级高于或等于该行要求的等级,才能看到"。但要注意两个代价:

  1. 调用者需要对 users 有 SELECT 权限,否则会收到指向 users 的 permission denied;
  2. READ COMMITTED 下可能出现竞态:子查询用的是查询开始时的快照,若并发事务正在调整权限,旧权限可能放行了新数据。
sql
-- 缓解方案之一:把逻辑封进 SECURITY DEFINER 函数
CREATE FUNCTION my_group_id() RETURNS int
LANGUAGE sql SECURITY DEFINER STABLE AS $$
    SELECT group_id FROM users WHERE user_name = current_user
$$;

CREATE POLICY fp_s2 ON information FOR SELECT
    USING (group_id <= my_group_id());

策略名的作用域 ​

策略名是表级的,不同表可以拥有同名策略,互不冲突:

sql
CREATE POLICY p_tenant ON orders   USING (tenant_id = 1);
CREATE POLICY p_tenant ON invoices USING (tenant_id = 1);   -- 合法

但同一张表内策略名必须唯一:

sql
CREATE POLICY p_tenant ON orders USING (tenant_id = 2);
text
ERROR:  policy "p_tenant" for table "orders" already exists

命名建议带上用途前缀(p_tenant_read、p_admin_all),避免几个月后面对一堆 p1、p2 无从下手。

ALTER POLICY 不能改什么 ​

这是最容易踩的坑之一。下面这些操作必须 DROP 后重建:

sql
-- 错误!ALTER POLICY 不支持改 FOR 命令类型
ALTER POLICY p_tenant ON orders FOR SELECT;

-- 错误!ALTER POLICY 不支持改 PERMISSIVE / RESTRICTIVE
ALTER POLICY p_tenant ON orders AS RESTRICTIVE;
text
ERROR:  syntax error at or near "FOR"

正确的做法:

sql
DROP POLICY p_tenant ON orders;
CREATE POLICY p_tenant ON orders FOR SELECT
    USING (tenant_id = current_setting('app.tenant_id')::int);

变更命令类型时务必注意"窗口期":从 DROP 到重建之间,策略缺失会短暂造成默认拒绝或越权,应放在同一个事务里执行。

sql
BEGIN;
DROP POLICY p_tenant ON orders;
CREATE POLICY p_tenant ON orders FOR SELECT
    USING (tenant_id = current_setting('app.tenant_id')::int);
COMMIT;

用 DO 块做幂等部署 ​

在自动化部署脚本里,反复执行 CREATE POLICY 会报"已存在"。用 DROP IF EXISTS + CREATE 组合,或写成幂等的 DO 块:

sql
BEGIN;
DROP POLICY IF EXISTS p_tenant ON orders;
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);

DROP POLICY IF EXISTS p_not_deleted ON orders;
CREATE POLICY p_not_deleted ON orders AS RESTRICTIVE
    USING (is_deleted = false);
COMMIT;

DO 块版本(适合放进迁移脚本):

sql
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_policies
        WHERE schemaname = 'public'
          AND tablename  = 'orders'
          AND policyname = 'p_tenant'
    ) THEN
        EXECUTE $p$
            CREATE POLICY p_tenant ON orders TO app_user
                USING (tenant_id = current_setting('app.tenant_id')::int)
        $p$;
    END IF;
END
$$;

这里必须用 EXECUTE 动态执行:CREATE POLICY 属于 DDL,在 PL/pgSQL 中需要通过动态 SQL 提交。无论用哪种写法,幂等部署的推荐方案始终是 DROP POLICY IF EXISTS + CREATE POLICY 放在同一事务里,语义最直白、行为最可预测。

策略与 GRANT 的配合:一张表两种控制 ​

目标手段
这个角色能不能"操作这张表"GRANT / REVOKE
这个角色能操作"哪些行"CREATE POLICY
这个角色能操作"哪些列"列级 GRANT
sql
-- 表级:允许增删改查
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;

-- 列级:只允许改这两列(其余列即使有表级 UPDATE 也改不了)
GRANT UPDATE (amount, remark) ON orders TO app_user;

-- 行级:只能操作本租户的行
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);

三者叠加,才能表达出"本租户、非软删除、只能改金额和备注"这样的完整规则。

常见错误的报错对照 ​

错误写法报错信息原因
FOR INSERT ... USING (...)"USING" not allowed for INSERT policyINSERT 无已有行
FOR SELECT ... WITH CHECK (...)"WITH CHECK" not allowed for SELECT policySELECT 无新行
FOR DELETE ... WITH CHECK (...)"WITH CHECK" not allowed for DELETE policyDELETE 无新行
USING (count(*) > 0)aggregate functions are not allowed in policy expressions策略表达式禁聚合
表名不存在relation "xxx" does not exist表名 / schema 写错
非属主执行must be owner of table xxx只有属主能管理策略
同名策略policy "xxx" for table "yyy" already exists策略名表内唯一

row_security = off 时会话内的策略检查改为报错,报错信息形如:

text
ERROR:  query would be affected by row-level security policy for table "orders"

实战示例 ​

示例一:把一套权限需求翻译成策略 ​

需求:orders 表是多租户共享表,需要满足

  1. 应用账号 app_user 只能读写本租户行;
  2. 租户管理员 tenant_admin 能读本租户全部行(含软删除);
  3. 软删除的行对所有人不可见;
  4. 任何写入都不能把行写到别的租户去。
sql
-- 表结构
CREATE TABLE orders (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_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);

-- 权限:表级 GRANT
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
GRANT SELECT                          ON orders TO tenant_admin;

-- 开关
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE  ROW LEVEL SECURITY;

-- 需求 1:应用账号本租户读写
CREATE POLICY p_app_rw ON orders AS PERMISSIVE FOR ALL TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

-- 需求 2:租户管理员本租户可读(含软删除,见需求 3 的例外处理)
CREATE POLICY p_admin_read ON orders AS PERMISSIVE FOR SELECT TO tenant_admin
    USING (tenant_id = current_setting('app.tenant_id')::int);

-- 需求 3:软删除对 app_user 一律不可见
CREATE POLICY p_hide_deleted ON orders AS RESTRICTIVE TO app_user
    USING (is_deleted = false);

验证:

sql
BEGIN;
SET LOCAL app.tenant_id = '7';

-- 以 app_user 身份
SET LOCAL ROLE app_user;
SELECT id, tenant_id, amount FROM orders ORDER BY id;
text
 id | tenant_id | amount
----+-----------+---------
  3 |         7 |   99.00
  8 |         7 |  150.00
(2 rows)

试插入别的租户(需求 4):

sql
INSERT INTO orders (tenant_id, amount) VALUES (8, 10.00);
text
ERROR:  new row violates row-level security policy for table "orders"

示例二:给报表角色开一个只读窗口 ​

需求:报表角色 r_report 需要看全部租户的汇总数据,但只允许 SELECT,且不允许看到金额为负的数据(数据质量问题)。

sql
CREATE ROLE r_report LOGIN PASSWORD 'change-me';
GRANT SELECT ON orders TO r_report;

-- 宽松:报表角色可读全部行
CREATE POLICY p_report_read ON orders AS PERMISSIVE FOR SELECT TO r_report
    USING (true);

-- 限制:所有角色都必须过滤掉金额为负的脏数据
CREATE POLICY p_amount_guard ON orders AS RESTRICTIVE
    USING (amount >= 0);

以报表角色查询:

sql
SET ROLE r_report;
SELECT tenant_id, count(*), sum(amount)
FROM orders GROUP BY tenant_id ORDER BY tenant_id;
text
 tenant_id | count |   sum
-----------+-------+----------
         7 |     2 |   249.00
         8 |     5 |  1830.50
        12 |     3 |   420.00
(3 rows)

对比应用账号,看到的行被 p_app_rw + p_hide_deleted + p_amount_guard 三重过滤:

sql
SET ROLE app_user;
SET app.tenant_id = '8';
SELECT count(*) FROM orders;
text
 count
-------
     4

(同一租户下原本 5 行,其中 1 行金额为负被 p_amount_guard 过滤。)

把这个"数据质量兜底"和"租户隔离"拆成 RESTRICTIVE + PERMISSIVE,是 RLS 组合能力的典型用法:隔离用宽松策略表达"能看到什么",全局约束用限制策略表达"谁都别想看到什么"。

注意事项 ​

策略要生效必须先 ENABLE

CREATE POLICY 本身不会打开开关,也不会有任何提示:

sql
CREATE POLICY p ON orders USING (tenant_id = 1);
-- 必须显式启用
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

默认是 PERMISSIVE,别误以为默认最严格

不写 AS 时策略是宽松的,多条之间是 OR,会放大可见范围:

sql
-- 两条宽松策略叠加 => 看到两个租户的数据
CREATE POLICY p1 ON orders USING (tenant_id = 1);
CREATE POLICY p2 ON orders USING (tenant_id = 2);   -- 不推荐:本意可能是限制

至少保留一条 PERMISSIVE

只有 RESTRICTIVE 时,没有任何"放行"来源,结果是全拒绝:

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

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

这是语法级硬限制:

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

TO CURRENT_USER 会被固化,不等于动态判断

想表达"只看自己的行",要写在 USING 里:

sql
-- 危险:固化成了创建策略时的用户
CREATE POLICY p_bad ON messages TO CURRENT_USER USING (true);
-- 正确
CREATE POLICY p_ok ON messages USING (owner = current_user);

ALTER POLICY 改不了命令类型和宽松属性

需要 DROP 重建,且应放在同一事务中避免窗口期:

sql
-- 错误!
ALTER POLICY p ON orders AS RESTRICTIVE;

策略表达式禁用聚合与窗口函数

sql
-- 错误!
CREATE POLICY p ON orders USING (amount > avg(amount));

策略里的子查询会带来权限与竞态问题

调用者必须对被引用的表有权限,且注意 READ COMMITTED 的快照语义:

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

管理策略需要表属主身份,GRANT 帮不上忙

即使是库的拥有者,非表属主也改不了策略:

sql
-- 报 must be owner of table xxx
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

命名要有语义,并且定期审计

p1、p2 这类命名在生产环境是灾难:

sql
-- 推荐命名:用途 + 命令 + 角色
-- p_tenant_read_app / p_admin_all / p_hide_deleted
SELECT tablename, policyname, permissive, roles, cmd
FROM pg_policies ORDER BY 1, 2;

最佳实践 ​

所有子句都显式写出

把 AS、FOR、TO、USING、WITH CHECK 全部写明,杜绝依赖默认值:

sql
CREATE POLICY p_tenant_rw ON orders
    AS PERMISSIVE
    FOR ALL
    TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

用 USING + WITH CHECK 双写防止"改出"隔离域

只写 USING 时,也允许用户把自己的行改到别人的租户,务必显式约束新行:

sql
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int)

隔离维度尽量用单行判断 + 索引列

避免在策略里回查其他表,让条件能走索引:

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

全局约束用 RESTRICTIVE,业务放行用 PERMISSIVE

角色清晰、可组合:

sql
CREATE POLICY p_all_read   ON orders AS PERMISSIVE USING (true);
CREATE POLICY p_no_deleted ON orders AS RESTRICTIVE USING (is_deleted = false);

会话上下文用 SET LOCAL + 自定义 GUC

事务级生效、自动复位,连接池场景下不会串租户:

sql
BEGIN;
SET LOCAL app.tenant_id = '7';
SELECT * FROM orders;
COMMIT;

部署脚本幂等化

用 DROP POLICY IF EXISTS + CREATE POLICY,包在一个事务里:

sql
BEGIN;
DROP POLICY IF EXISTS p_tenant ON orders;
CREATE POLICY p_tenant ON orders TO app_user
    USING (tenant_id = current_setting('app.tenant_id')::int);
COMMIT;

策略与 GRANT 一套脚本管理

把表级权限、列级权限、策略写在同一个迁移脚本里,避免"策略在、权限不在"的半成品状态:

sql
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE  ROW LEVEL SECURITY;

为每条策略写验证查询

策略上线前,用 SET ROLE + SET LOCAL 模拟不同身份跑通正反两个用例:

sql
-- 正例:能看到自己的
SET LOCAL ROLE app_user; SET LOCAL app.tenant_id = '7';
SELECT count(*) FROM orders;

-- 反例:看不到别人的
SELECT count(*) FROM orders WHERE tenant_id = 8;   -- 期望 0

用 pg_policies 做例行审计

纳入监控脚本,防止有人偷偷 DROP POLICY 或 DISABLE:

sql
SELECT count(*) FROM pg_policies WHERE tablename = 'orders';

SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname IN ('orders', 'invoices', 'users');

改策略前先看执行计划

新建或修改策略后,用 EXPLAIN 确认索引仍被使用、计划没有退化:

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

不要把策略逻辑与应用逻辑重复实现

RLS 是唯一可信来源。应用层再写一遍 WHERE tenant_id = ... 只会造成"两处定义、一处出错":

sql
-- 推荐:应用只管设置上下文
SET LOCAL app.tenant_id = '7';
SELECT * FROM orders;   -- 不写 tenant_id 条件

下一节

继续学习:USING 与 WITH CHECK 详解

参考链接 ​