文章目录
-
- [一、RLS 概述](#一、RLS 概述)
-
- [1.1 RLS 基本原理](#1.1 RLS 基本原理)
- [1.2 启用与配置 RLS](#1.2 启用与配置 RLS)
- [1.3 适用场景与局限](#1.3 适用场景与局限)
- [1.4 实践建议](#1.4 实践建议)
- 二、实战案例
-
- [场景1:多租户 SaaS 应用](#场景1:多租户 SaaS 应用)
-
- [1. 建表并启用 RLS](#1. 建表并启用 RLS)
- [2. 创建策略:用户只能访问其租户的项目](#2. 创建策略:用户只能访问其租户的项目)
- [3. 应用层设置租户上下文](#3. 应用层设置租户上下文)
- [4. 测试效果](#4. 测试效果)
- 场景2:员工只能查看自己的薪资
- 三、高级用法
-
- [3.1 不同操作使用不同条件](#3.1 不同操作使用不同条件)
- [3.2 多策略组合](#3.2 多策略组合)
- [3.3 强制对表所有者生效](#3.3 强制对表所有者生效)
- [3.4 使用函数封装复杂逻辑](#3.4 使用函数封装复杂逻辑)
- 四、性能影响与优化
-
- [4.1 执行计划变化](#4.1 执行计划变化)
- [4.2 索引建议](#4.2 索引建议)
- [4.3 避免复杂函数](#4.3 避免复杂函数)
- 五、管理与调试
-
- [5.1 查看现有策略](#5.1 查看现有策略)
- [5.2 临时禁用 RLS(调试用)](#5.2 临时禁用 RLS(调试用))
- [5.3 日志监控](#5.3 日志监控)
在 PostgreSQL 中,行级安全(Row-Level Security,简称 RLS) 是一种强大的访问控制机制,允许数据库管理员基于行数据的内容,动态限制用户对表中特定行的 SELECT、INSERT、UPDATE 或 DELETE 操作。RLS 使得多租户应用、数据隔离、隐私保护等场景无需在应用层硬编码过滤逻辑,而是在数据库层实现细粒度安全控制。
本文将系统讲解 RLS 的原理、配置方法、使用场景、性能影响及最佳实践。
一、RLS 概述
1.1 RLS 基本原理
PostgreSQL 的行级安全(RLS)提供了一种声明式、高效且安全的行过滤机制,将数据访问控制下沉到数据库层,显著降低应用复杂度并提升安全性。通过合理设计策略、配合索引优化和会话上下文管理,RLS 能够优雅支撑多租户、隐私合规等复杂业务需求。
关键原则:启用 RLS → 定义策略 → 设置会话上下文 → 验证与监控。
默认情况下,PostgreSQL 的权限控制是表级别 的:用户要么能访问整张表,要么不能。RLS 则在此基础上增加了一层行级别过滤:
- 当 RLS 启用后,所有对该表的 DML 操作(SELECT/INSERT/UPDATE/DELETE)都会自动附加一个由策略定义的 WHERE 条件。
- 该条件基于当前用户、会话变量、行数据本身等上下文动态生成。
- 即使用户拥有表的 SELECT 权限,若其操作不满足 RLS 策略,相关行将被静默过滤(不报错,仅不可见)。
注意:超级用户(superuser)和表所有者(table owner)默认绕过 RLS。可通过
ALTER TABLE ... FORCE ROW LEVEL SECURITY强制对其生效。
1.2 启用与配置 RLS
1、启用 RLS
sql
-- 对表启用 RLS(默认策略为"拒绝所有",除非定义了策略)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
启用后,若未定义任何策略,则所有用户(包括表所有者)都无法访问该表(超级用户除外)。
2、创建安全策略
使用 CREATE POLICY 定义访问规则。语法如下:
sql
CREATE POLICY policy_name ON table_name
[FOR {ALL | SELECT | INSERT | UPDATE | DELETE}]
[TO {role_name | PUBLIC | CURRENT_USER | SESSION_USER}]
[USING (condition)]
[WITH CHECK (condition)];
USING:用于 SELECT、UPDATE、DELETE 的行过滤条件。WITH CHECK:用于 INSERT 和 UPDATE 的新行校验条件(确保插入/更新后的行仍符合策略)。- 若省略
FOR,默认为ALL(即同时作用于所有操作)。 - 若省略
TO,默认为PUBLIC(所有用户)。
1.3 适用场景与局限
1、适用场景
- 多租户 SaaS 应用(数据隔离)
- 医疗/金融系统(患者/客户数据隐私)
- 内部系统(员工只能看本部门数据)
- GDPR/CCPA 合规(限制个人数据访问)
2、局限与注意事项
- 不适用于临时表。
- COPY 命令绕过 RLS(因 COPY 是底层数据导入,非 SQL 操作)。
- 逻辑复制(Logical Replication)可能受影响:订阅端需同步 RLS 策略。
- 外键约束不受 RLS 影响:即使子表行因 RLS 不可见,外键仍会校验。
- 视图上的 RLS:若视图基于启用了 RLS 的表,策略仍会生效。
1.4 实践建议
-
始终通过会话变量传递上下文
使用
SET app.tenant_id = 'xxx'而非在每条 SQL 中传参,避免应用层遗漏。 -
策略条件尽量简单
避免在
USING中调用高开销函数或子查询。 -
为 RLS 字段建立索引
尤其是高频过滤的租户 ID、用户 ID 等。
-
测试覆盖所有角色和边界情况
包括:无权限用户、跨租户访问、插入非法数据等。
-
不要依赖 RLS 实现完整安全体系
RLS 是访问控制的最后一道防线,应用层仍需做基本校验。
-
文档化策略逻辑
在代码注释或数据库文档中说明 RLS 规则,便于维护。
二、实战案例
场景1:多租户 SaaS 应用
假设有一张 projects 表,每个项目属于一个租户(tenant_id),要求用户只能访问自己租户的数据。
1. 建表并启用 RLS
sql
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
2. 创建策略:用户只能访问其租户的项目
sql
CREATE POLICY tenant_isolation_policy ON projects
FOR ALL
TO PUBLIC
USING (tenant_id = current_setting('app.current_tenant'));
这里使用
current_setting('app.current_tenant')获取会话级租户 ID(需提前设置)。
3. 应用层设置租户上下文
在每次数据库会话开始时,由应用设置租户标识:
sql
SET app.current_tenant = 'acme_corp';
此后,所有查询自动附加 WHERE tenant_id = 'acme_corp'。
4. 测试效果
sql
-- 用户只能看到自己租户的项目
SELECT * FROM projects; -- 自动过滤
-- 插入时自动校验 tenant_id
INSERT INTO projects (tenant_id, name) VALUES ('other_tenant', 'Bad Project');
-- 报错:new row violates row-level security policy for table "projects"
场景2:员工只能查看自己的薪资
sql
CREATE TABLE salaries (
emp_id INT,
amount NUMERIC,
visible_to TEXT -- 可见范围:'self', 'manager', 'hr'
);
ALTER TABLE salaries ENABLE ROW LEVEL SECURITY;
-- 策略:普通员工只能看自己的;HR 可看全部
CREATE POLICY salary_view_policy ON salaries
FOR SELECT
TO employee_role
USING (emp_id = current_user_id() OR visible_to = 'hr');
-- 假设存在函数 current_user_id() 返回当前用户ID
三、高级用法
3.1 不同操作使用不同条件
sql
CREATE POLICY project_policy ON projects
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant'))
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant'));
3.2 多策略组合
可为同一表创建多个策略,PostgreSQL 会按以下规则合并:
- 同一角色、同一操作类型的多个策略 → OR 关系
- 不同角色的策略 → 各自独立生效
示例:允许用户访问自己或公开的项目
sql
CREATE POLICY user_projects ON projects
FOR SELECT
TO app_user
USING (owner_id = current_user_id());
CREATE POLICY public_projects ON projects
FOR SELECT
TO app_user
USING (is_public = true);
最终条件:owner_id = ? OR is_public = true
3.3 强制对表所有者生效
sql
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
此后,即使表所有者执行查询,也受 RLS 限制。
3.4 使用函数封装复杂逻辑
sql
CREATE FUNCTION can_access_project(proj_id INT) RETURNS BOOLEAN AS $$
BEGIN
-- 复杂业务逻辑:检查角色、时间、审批状态等
RETURN EXISTS (SELECT 1 FROM access_rules WHERE ...);
END;
$$ LANGUAGE plpgsql STABLE;
CREATE POLICY complex_policy ON projects
USING (can_access_project(id));
注意:函数应标记为
STABLE或IMMUTABLE,避免在每行调用时产生副作用。
四、性能影响与优化
4.1 执行计划变化
RLS 策略会被合并到查询的 WHERE 条件中,可能影响索引选择。
示例:
sql
-- 无 RLS 时
SELECT * FROM projects WHERE name = 'Alpha'; -- 可走 name 索引
-- 有 RLS 时(策略:tenant_id = ?)
-- 实际执行:SELECT * FROM projects WHERE tenant_id = ? AND name = 'Alpha';
-- 需要复合索引 (tenant_id, name)
4.2 索引建议
-
为 RLS 过滤字段(如
tenant_id)创建索引。 -
若常与其它条件组合查询,创建复合索引 :
sqlCREATE INDEX idx_projects_tenant_name ON projects (tenant_id, name);
4.3 避免复杂函数
RLS 条件中的函数若计算开销大,会导致全表扫描。应尽量使用简单列比较。
五、管理与调试
5.1 查看现有策略
sql
-- 查看某表的所有策略
SELECT * FROM pg_policies WHERE tablename = 'projects';
-- 或使用 psql 命令
\d+ projects
5.2 临时禁用 RLS(调试用)
sql
-- 会话级禁用(需 superuser 或 bypassrls 权限)
SET row_security TO OFF;
-- 恢复
SET row_security TO ON;
普通用户无法关闭 RLS,除非被授予
BYPASSRLS权限:
sqlALTER ROLE auditor WITH BYPASSRLS;
5.3 日志监控
开启日志可观察 RLS 是否生效:
conf
# postgresql.conf
log_statement = 'all'
查询日志中会显示实际执行的带过滤条件的 SQL。