导读:本期聚焦于乐少创作的《PostgreSQL行级安全策略RLS怎么用?如何实现细粒度数据访问控制》,敬请观看详情。当多个租户共享同一张数据表时,如何在数据库层面保证每个用户只能看到属于自己的数据?PostgreSQL的行级安全策略RLS提供了一种优雅的解决方案。本文将深入讲解RLS的底层工作机制,包括策略的启用方式、USING与WITH CHECK子句的区别、结合当前用户上下文动态过滤数据的技巧,以及多租户场景下的实战配置。同时会分析RLS对查询性能的影响、常见的超级用户绕过问题与防御方法,帮助你在应用层之外构建一道数据库内部的数据隔离防线。

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

PostgreSQL行级安全策略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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260902/49114.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。