创建与管理策略
上一节讲清了 RLS 的整体机制,本节聚焦最日常的工作:如何把一条策略准确写出来。
CREATE POLICY 看着只有六七个可选子句,但要写得对、写得安全,必须搞清楚三件事:策略的属性(AS 与 FOR)、策略的作用对象(TO)、以及策略的判断条件(USING / WITH CHECK)。子句顺序有讲究,命令类型与表达式之间有硬性搭配限制,角色列表的展开规则也容易踩坑。
本节把 CREATE POLICY 的完整语法拆开逐项讲解,再用真实场景演示如何把业务需求翻译成策略。
前置知识
阅读本节前,建议先了解:行级安全(RLS)概览
基础概念
一条策略由三部分组成
CREATE POLICY 策略名 ON 表名
├── ① 属性:AS PERMISSIVE | RESTRICTIVE + FOR ALL | SELECT | INSERT | UPDATE | DELETE
├── ② 对象:TO 角色列表
└── ③ 条件:USING (...) / WITH CHECK (...)- 属性决定"这条策略怎么参与组合、管哪类命令";
- 对象决定"这条策略对哪些角色生效";
- 条件决定"哪些行放行"。
三个部分全都有默认值:不写 AS 默认 PERMISSIVE,不写 FOR 默认 ALL,不写 TO 默认 PUBLIC。
语法骨架
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:
-- 正确:每次查询时按执行者动态判断
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 | ✅ | — | 可省略 | 不允许 |
记住三条:
INSERT没有"已有行",所以不能写USING;SELECT/DELETE不产生新行,所以不能写WITH CHECK;ALL和UPDATE两者都能写,若不写WITH CHECK,则USING兼任新行校验。
语法与用法
最小策略
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 同时作用于选中行与新写入行。
完整策略
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);六个子句全写上,是排查问题时的推荐写法:所有默认行为都被显式化,读代码的人不需要脑内补默认值。
只读策略
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 各管一段。
只写策略
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 策略。
更新策略
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 决定"改完之后的行还合法吗"。两者都写可以防止把行"改出"自己的租户。
修改策略
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 ) ]-- 只改角色列表
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 宽松 / 限制属性,必须删除重建。同时,被省略的子句保持原样不变。
删除策略
-- 单条删除
DROP POLICY p_tenant ON orders;
-- 幂等写法
DROP POLICY IF EXISTS p_tenant ON orders;
-- 一条语句删除多个
DROP POLICY IF EXISTS p_a, p_b ON orders;查看表上有哪些策略
SELECT policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND tablename = 'orders'
ORDER BY policyname; 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 开关
SELECT relname,
relrowsecurity,
relforcerowsecurity,
pg_get_userbyid(relowner) AS owner
FROM pg_class
WHERE relname = 'orders'; relname | relrowsecurity | relforcerowsecurity | owner
---------+----------------+---------------------+---------
orders | t | t | dba_app
(1 row)relrowsecurity 对应 ENABLE,relforcerowsecurity 对应 FORCE。
详细说明
省略 USING 会发生什么
如果一条策略既没写 USING 也没写 WITH CHECK,它等价于 USING (true):
-- 这条策略对所有行都放行,对 PUBLIC 生效
CREATE POLICY p_open ON orders FOR SELECT;这在"我只想给某个角色开个口子"时确实有用,但极容易造成越权。建议任何面向业务表的策略都显式写出 USING,把 USING (true) 留给确实需要全权放行的管理员策略。
-- 明确表达意图:管理员全权
CREATE POLICY p_admin ON orders TO admin_role FOR ALL
USING (true) WITH CHECK (true);角色列表的展开与继承
TO 里可以列多个角色,角色继承关系正常生效:
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:需要读旧行与新行。
-- 用户既要能 "看到" 行(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 各自的作用点
┌─────────────────────────────────────────┐
已有行 ──────▶│ USING(这个行我能看到 / 我能操作吗?) │────▶ 放行
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
新行 ──────▶│ WITH CHECK(这行我能写进去吗?) │────▶ 写入
└─────────────────────────────────────────┘一句话:USING 管"看得见",WITH CHECK 管"写得进"。USING 返回 false 或 NULL 时行被静默过滤;WITH CHECK 返回 false 或 NULL 时直接报错。
表达式的能力与限制
策略表达式必须是返回布尔的 SQL 条件表达式,并且:
- 不能含聚合函数(
count()、sum()等); - 不能含窗口函数(
row_number()等); - 可以调用函数,但该函数对调用者必须可见、可执行;
- 可以引用当前行的任意列;
- 可以引用
current_user、current_setting()等会话信息。
-- 合法
CREATE POLICY p1 ON orders USING (amount > 0 AND tenant_id = 1);
-- 错误!策略表达式不能含聚合
CREATE POLICY p2 ON orders USING (amount > avg(amount));ERROR: aggregate functions are not allowed in policy expressions想用"平均值"这类派生条件,正确做法是在写入时物化到列上,或存到一个配置表里再引用。
用子查询实现"看别人"的权限
USING 里允许子查询,这打开了更复杂的权限模型。官方经典示例是"按用户的安全等级过滤信息表":
CREATE POLICY fp_s ON information FOR SELECT
USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));这行策略表达"我的等级高于或等于该行要求的等级,才能看到"。但要注意两个代价:
- 调用者需要对
users有SELECT权限,否则会收到指向users的permission denied; READ COMMITTED下可能出现竞态:子查询用的是查询开始时的快照,若并发事务正在调整权限,旧权限可能放行了新数据。
-- 缓解方案之一:把逻辑封进 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());策略名的作用域
策略名是表级的,不同表可以拥有同名策略,互不冲突:
CREATE POLICY p_tenant ON orders USING (tenant_id = 1);
CREATE POLICY p_tenant ON invoices USING (tenant_id = 1); -- 合法但同一张表内策略名必须唯一:
CREATE POLICY p_tenant ON orders USING (tenant_id = 2);ERROR: policy "p_tenant" for table "orders" already exists命名建议带上用途前缀(p_tenant_read、p_admin_all),避免几个月后面对一堆 p1、p2 无从下手。
ALTER POLICY 不能改什么
这是最容易踩的坑之一。下面这些操作必须 DROP 后重建:
-- 错误!ALTER POLICY 不支持改 FOR 命令类型
ALTER POLICY p_tenant ON orders FOR SELECT;
-- 错误!ALTER POLICY 不支持改 PERMISSIVE / RESTRICTIVE
ALTER POLICY p_tenant ON orders AS RESTRICTIVE;ERROR: syntax error at or near "FOR"正确的做法:
DROP POLICY p_tenant ON orders;
CREATE POLICY p_tenant ON orders FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::int);变更命令类型时务必注意"窗口期":从 DROP 到重建之间,策略缺失会短暂造成默认拒绝或越权,应放在同一个事务里执行。
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 块:
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 块版本(适合放进迁移脚本):
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 |
-- 表级:允许增删改查
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 policy | INSERT 无已有行 |
FOR SELECT ... WITH CHECK (...) | "WITH CHECK" not allowed for SELECT policy | SELECT 无新行 |
FOR DELETE ... WITH CHECK (...) | "WITH CHECK" not allowed for DELETE policy | DELETE 无新行 |
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 时会话内的策略检查改为报错,报错信息形如:
ERROR: query would be affected by row-level security policy for table "orders"实战示例
示例一:把一套权限需求翻译成策略
需求:orders 表是多租户共享表,需要满足
- 应用账号
app_user只能读写本租户行; - 租户管理员
tenant_admin能读本租户全部行(含软删除); - 软删除的行对所有人不可见;
- 任何写入都不能把行写到别的租户去。
-- 表结构
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);验证:
BEGIN;
SET LOCAL app.tenant_id = '7';
-- 以 app_user 身份
SET LOCAL ROLE app_user;
SELECT id, tenant_id, amount FROM orders ORDER BY id; id | tenant_id | amount
----+-----------+---------
3 | 7 | 99.00
8 | 7 | 150.00
(2 rows)试插入别的租户(需求 4):
INSERT INTO orders (tenant_id, amount) VALUES (8, 10.00);ERROR: new row violates row-level security policy for table "orders"示例二:给报表角色开一个只读窗口
需求:报表角色 r_report 需要看全部租户的汇总数据,但只允许 SELECT,且不允许看到金额为负的数据(数据质量问题)。
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);以报表角色查询:
SET ROLE r_report;
SELECT tenant_id, count(*), sum(amount)
FROM orders GROUP BY tenant_id ORDER BY tenant_id; 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 三重过滤:
SET ROLE app_user;
SET app.tenant_id = '8';
SELECT count(*) FROM orders; count
-------
4(同一租户下原本 5 行,其中 1 行金额为负被 p_amount_guard 过滤。)
把这个"数据质量兜底"和"租户隔离"拆成 RESTRICTIVE + PERMISSIVE,是 RLS 组合能力的典型用法:隔离用宽松策略表达"能看到什么",全局约束用限制策略表达"谁都别想看到什么"。
注意事项
策略要生效必须先 ENABLE
CREATE POLICY 本身不会打开开关,也不会有任何提示:
CREATE POLICY p ON orders USING (tenant_id = 1);
-- 必须显式启用
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;默认是 PERMISSIVE,别误以为默认最严格
不写 AS 时策略是宽松的,多条之间是 OR,会放大可见范围:
-- 两条宽松策略叠加 => 看到两个租户的数据
CREATE POLICY p1 ON orders USING (tenant_id = 1);
CREATE POLICY p2 ON orders USING (tenant_id = 2); -- 不推荐:本意可能是限制至少保留一条 PERMISSIVE
只有 RESTRICTIVE 时,没有任何"放行"来源,结果是全拒绝:
-- 错误用法:只有限制策略,谁都看不到
CREATE POLICY p_only_r ON orders AS RESTRICTIVE USING (is_deleted = false);INSERT 不能写 USING,SELECT/DELETE 不能写 WITH CHECK
这是语法级硬限制:
-- 错误!
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 里:
-- 危险:固化成了创建策略时的用户
CREATE POLICY p_bad ON messages TO CURRENT_USER USING (true);
-- 正确
CREATE POLICY p_ok ON messages USING (owner = current_user);ALTER POLICY 改不了命令类型和宽松属性
需要 DROP 重建,且应放在同一事务中避免窗口期:
-- 错误!
ALTER POLICY p ON orders AS RESTRICTIVE;策略表达式禁用聚合与窗口函数
-- 错误!
CREATE POLICY p ON orders USING (amount > avg(amount));策略里的子查询会带来权限与竞态问题
调用者必须对被引用的表有权限,且注意 READ COMMITTED 的快照语义:
-- 若调用者无权访问 my_depts,查询会报 permission denied
CREATE POLICY p ON employees USING (dept_id IN (SELECT dept_id FROM my_depts));管理策略需要表属主身份,GRANT 帮不上忙
即使是库的拥有者,非表属主也改不了策略:
-- 报 must be owner of table xxx
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;命名要有语义,并且定期审计
p1、p2 这类命名在生产环境是灾难:
-- 推荐命名:用途 + 命令 + 角色
-- 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 全部写明,杜绝依赖默认值:
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 时,也允许用户把自己的行改到别人的租户,务必显式约束新行:
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int)隔离维度尽量用单行判断 + 索引列
避免在策略里回查其他表,让条件能走索引:
CREATE INDEX idx_orders_tenant ON orders (tenant_id);
USING (tenant_id = current_setting('app.tenant_id')::int)全局约束用 RESTRICTIVE,业务放行用 PERMISSIVE
角色清晰、可组合:
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
事务级生效、自动复位,连接池场景下不会串租户:
BEGIN;
SET LOCAL app.tenant_id = '7';
SELECT * FROM orders;
COMMIT;部署脚本幂等化
用 DROP POLICY IF EXISTS + CREATE POLICY,包在一个事务里:
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 一套脚本管理
把表级权限、列级权限、策略写在同一个迁移脚本里,避免"策略在、权限不在"的半成品状态:
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 模拟不同身份跑通正反两个用例:
-- 正例:能看到自己的
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:
SELECT count(*) FROM pg_policies WHERE tablename = 'orders';
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname IN ('orders', 'invoices', 'users');改策略前先看执行计划
新建或修改策略后,用 EXPLAIN 确认索引仍被使用、计划没有退化:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE tenant_id = 7;不要把策略逻辑与应用逻辑重复实现
RLS 是唯一可信来源。应用层再写一遍 WHERE tenant_id = ... 只会造成"两处定义、一处出错":
-- 推荐:应用只管设置上下文
SET LOCAL app.tenant_id = '7';
SELECT * FROM orders; -- 不写 tenant_id 条件下一节