在传统的权限体系中,PostgreSQL的GRANT语句只能控制到表级别,也就是允许或拒绝某个角色对整张表执行SELECT、INSERT、UPDATE等操作。但在多租户系统、企业内部数据分级等场景中,往往需要更细的控制粒度:同一个表里,A部门的人只能看A部门的数据,租户X的用户绝对不能查到租户Y的记录。如果把这些逻辑全部放在应用层,一旦某个接口漏写WHERE条件,数据就会泄露。PostgreSQL从9.5版本开始提供的行级安全策略(Row Level Security,简称RLS),正是为了在数据库内核层面解决这个问题。

RLS的工作原理与基本启用方式
RLS的核心思想是:给表附加一组策略(Policy),当有查询访问这个表时,PostgreSQL会在执行阶段自动把策略表达式并入查询条件。对SELECT来说,策略等价于一个隐形的WHERE子句;对INSERT和UPDATE来说,策略还可以校验写入的行是否满足条件。整个过程发生在数据库引擎内部,应用程序无法绕过,也不需要修改任何SQL语句。
默认情况下,所有表的RLS都是关闭的,即使用户创建了策略也不会生效。必须先执行ALTER TABLE ... ENABLE ROW LEVEL SECURITY显式开启。来看一个最基础的例子:
-- 创建测试表并插入数据
CREATE TABLE orders (
id serial PRIMARY KEY,
tenant_id text NOT NULL,
amount numeric
);
INSERT INTO orders (tenant_id, amount) VALUES ('company_a', 100), ('company_b', 200);
-- 开启行级安全
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 创建策略:每个租户只能看到自己的订单
CREATE POLICY tenant_isolation ON orders
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant'));
-- 切换到普通用户测试(表的属主不受RLS约束)
SET ROLE app_user;
SET app.current_tenant = 'company_a';
SELECT * FROM orders; -- 只能看到 company_a 的记录上面代码中有一个非常关键的细节:current_setting读取的是会话级别的自定义变量。应用层在建立连接后执行SET app.current_tenant = 'xxx',后续该会话的所有查询都会自动被限制在对应租户范围内。这种模式被称为HOOK方案,是目前SaaS多租户系统中最常见的RLS实践。
USING与WITH CHECK的区别及策略类型详解
很多初学者对RLS的理解只停留在SELECT过滤,实际上策略可以精确控制六种命令类型:SELECT、INSERT、UPDATE、DELETE,以及ALL(匹配全部)。每条策略由两个子句构成,理解它们的区别是掌握RLS的关键。
USING子句作用于读取已有行的场景。它决定哪些行对当前查询可见:SELECT时哪些行能被返回,UPDATE和DELETE时哪些行能被修改或删除。WITH CHECK子句作用于产生新行的场景:INSERT的新行、UPDATE后产生的新版本行,都必须满足WITH CHECK表达式,否则操作会报错。一个容易踩的坑是:如果UPDATE策略只写了USING而没写WITH CHECK,PostgreSQL会默认用USING的表达式来校验新行。比如策略是USING (tenant_id = 'company_a'),用户把一行数据的tenant_id改成company_b时会失败,这个默认行为在大多数场景下是合理的,但你需要清楚它的存在。
-- 完整的读写分离策略
CREATE POLICY tenant_select ON orders
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant'));
CREATE POLICY tenant_insert ON orders
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant'));
-- UPDATE同时需要两个子句
CREATE POLICY tenant_update ON orders
FOR UPDATE
USING (tenant_id = current_setting('app.current_tenant'))
WITH CHECK (tenant_id = current_setting('app.current_tenant'));
-- DELETE只需要USING
CREATE POLICY tenant_delete ON orders
FOR DELETE
USING (tenant_id = current_setting('app.current_tenant'));另外还有两个修饰词值得注意:TO子句可以限定策略只对特定角色生效,例如TO app_user就把数据库管理员排除在过滤范围外;AS PERMISSIVE与AS RESTRICTIVE决定了多条策略之间是OR还是AND关系。默认是PERMISSIVE,即多条策略命中任意一条即可通过,而RESTRICTIVE策略必须全部满足才行,适合组合出"属于本租户且未被锁定"这类复合条件。
超级用户绕过风险与性能影响分析
RLS有几个必须了解的豁免规则。第一,超级用户(superuser)完全不受RLS约束;第二,表的属主默认也绕过策略,除非同时执行ALTER TABLE ... FORCE ROW LEVEL SECURITY强制属主也遵守;第三,具备BYPASSRLS属性的角色同样免疫。这意味着如果你的应用用一个高权限账号连接数据库,RLS形同虚设。正确的做法是为应用创建普通角色,只授予必要的表权限,并通过FORCE选项堵住属主漏洞:
-- 创建受限角色并授权 CREATE ROLE app_user LOGIN PASSWORD 'strong_password_here'; GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user; -- 强制表属主也遵守RLS ALTER TABLE orders FORCE ROW LEVEL SECURITY; -- 检查角色是否拥有危险属性 SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'app_user';
在性能方面,RLS的表达式会在查询规划阶段被合并进WHERE条件,因此它能否走索引完全取决于策略表达式本身。如果策略写的是tenant_id = current_setting('app.current_tenant'),而tenant_id上恰好有索引,那么查询计划和手写WHERE不会有本质区别,性能损失可以忽略。但如果策略里包含子查询,例如USING (tenant_id IN (SELECT tenant_id FROM user_tenants WHERE user_id = current_user)),每次执行都可能触发额外的子查询开销,此时建议把user_tenants的关联结果缓存到会话变量或物化视图中。
排查RLS相关性能问题时,EXPLAIN是第一工具。可以先以超级用户身份查看带策略后的真实执行计划,对比预计行数与实际行数的偏差。此外要注意,策略中调用的函数如果不是IMMUTABLE的,优化器无法在规划期做常量折叠,每行求值都会带来函数调用成本,能用表达式就尽量少封装函数。
典型应用场景与实施建议
RLS最常见的落地场景有三类。一是多租户SaaS隔离,即上文演示的tenant_id方案,配合连接池使用时要注意:PgBouncer这类中间件在事务 pooling模式下,SET设置的会话变量可能在下一个事务中被复用,必须改用SET LOCAL或在事务开头显式重设,否则会出现串租户的严重事故。二是企业内部数据分级,比如员工只能看自己创建的记录、经理能看整个部门的记录,用current_user关联组织架构表即可表达。三是简化ORM层的权限逻辑,把数据边界下沉到数据库后,业务代码里的每一个查询都不必再拼租户条件,漏写WHERE导致泄露的风险从根源上被消除。
实施RLS时建议遵循几条经验:策略表达式保持简单并与索引对齐;为每个业务角色设计独立的TO目标,避免用一条ALL策略包打天下;上线前用普通角色完整回归一遍增删改查路径;同时在监控中关注因WITH CHECK失败产生的错误日志,那往往是应用层写入越界数据的信号。只要设计得当,RLS能以极低的改造成本为系统提供一道坚实的数据库级数据隔离防线。
PostgreSQLRLS行级安全策略修改时间:2026-09-02 20:07:01