在数据库里做权限控制?重新理解 PostgreSQL 的 RLS(行级安全)

最近在做一个数据分析看板的需求,技术选型时考虑用 Supabase + nocode 平台快速搭建。翻 Supabase 文档时,发现它反复强调一个概念------RLS(Row-Level Security,行级安全),而且几乎所有表的默认建议都是"开启 RLS"。

我的第一反应是:权限控制不是应该在应用层做吗?在每个 API 里判断当前用户有没有权限访问这行数据,不是更灵活吗?为什么要把安全逻辑下沉到数据库层?

这个问题困扰了我一阵子,把 PostgreSQL 的 RLS 机制完整研究了一遍之后,才意识到------应用层做权限控制,其实一直有一个巨大的盲区。


先看一个痛点:应用层过滤的"漏网之鱼"

假设你在做一个多用户的数据看板系统,用户登录后只能看到自己的数据。最常见的做法是在每个查询里加 WHERE 条件:

sql 复制代码
-- 应用层拼接 SQL,加上用户过滤
SELECT * FROM analytics_data WHERE user_id = '当前登录用户的ID';

看起来没问题,但随着系统变复杂,问题开始浮现:

问题一:遗漏一个 WHERE,数据全泄露

系统有 50 个查询接口,其中 49 个都正确地加了 WHERE user_id = ?。但有一个新接口忘了加------也许是复制粘贴时漏了,也许是实习生写的代码没 review 到。

结果:任何登录用户都能看到所有人的数据。 一个遗漏 = 全量泄露。

问题二:动态 SQL 拼接容易出错

如果查询条件是动态拼接的,比如:

java 复制代码
StringBuilder sql = new StringBuilder("SELECT * FROM analytics_data WHERE 1=1");
if (userId != null) {
    sql.append(" AND user_id = '").append(userId).append("'");
}
// 如果 userId 为 null,这行就不加了......

userId 传了 null?过滤条件没了,全量数据返回。

问题三:直连数据库的工具绕过应用层

数据分析师想查数据,懒得走你的 API,直接用数据库客户端连上去写 SQL。这时候应用层的权限控制完全失效------因为他们根本没经过你的应用。

你可能会说:"那就不给他们数据库账号啊。" 但在 Supabase 的架构里,前端是直连数据库的(通过 PostgREST),这意味着前端请求直接打到 PostgreSQL,中间没有传统意义上的后端应用层。这种场景下,权限控制必须在数据库层做。


RLS 是什么:数据库层的"隐形 WHERE"

Row-Level Security(行级安全) 是 PostgreSQL 9.5 引入的特性。它的核心思想是:在数据库引擎层面,自动给每一条查询附加过滤条件,确保用户只能看到自己有权限的行。

开启 RLS 后,即使有人写了 SELECT * FROM analytics_data 不带任何条件,PostgreSQL 也会自动在底层加上过滤------你忘了加 WHERE,数据库替你加。

最小示例

sql 复制代码
-- 1. 建表
CREATE TABLE analytics_data (
    id BIGSERIAL PRIMARY KEY,
    user_id TEXT NOT NULL,
    metric_name TEXT NOT NULL,
    metric_value NUMERIC,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 2. 插入一些数据
INSERT INTO analytics_data (user_id, metric_name, metric_value) VALUES
    ('alice', 'page_views', 1200),
    ('bob', 'page_views', 800),
    ('alice', 'clicks', 350),
    ('bob', 'clicks', 200);

-- 3. 开启 RLS
ALTER TABLE analytics_data ENABLE ROW LEVEL SECURITY;

-- 4. 创建策略:用户只能看到自己的数据
CREATE POLICY user_isolation_policy ON analytics_data
    FOR SELECT
    USING (user_id = current_user);

现在,用 alice 身份查询:

sql 复制代码
-- alice 登录后执行
SELECT * FROM analytics_data;

结果:

yaml 复制代码
 id | user_id | metric_name | metric_value | created_at
----+---------+-------------+--------------+------------
  1 | alice   | page_views  |         1200 | 2026-07-06 ...
  3 | alice   | clicks      |          350 | 2026-07-06 ...

alice 写的是 SELECT *,没有加任何条件,但只看到了自己的两行。数据库引擎自动在底层加了 WHERE user_id = current_user

bob 的数据对 alice 完全不可见,就像不存在一样。


RLS 的核心机制

Policy(策略)

Policy 是 RLS 的核心。你可以把它理解为数据库层面的隐式 WHERE 条件 ,但它比 WHERE 更智能------它会根据当前连接的用户身份动态生成。

sql 复制代码
CREATE POLICY 策略名称 ON 表名
    FOR {SELECT | INSERT | UPDATE | DELETE | ALL}  -- 操作类型
    USING (条件表达式)         -- 控制哪些行"可见"
    WITH CHECK (条件表达式);   -- 控制哪些行"可写入"

两个关键子句:

  • USING :作用于 SELECTUPDATEDELETE------决定用户能看到/操作哪些行
  • WITH CHECK :作用于 INSERTUPDATE------决定用户能写入哪些行(写入后必须满足此条件)

一个表可以有多个 Policy

sql 复制代码
-- 策略1:用户只能看自己的数据
CREATE POLICY view_own_data ON analytics_data
    FOR SELECT
    USING (user_id = current_user);

-- 策略2:admin 可以看所有数据
CREATE POLICY admin_view_all ON analytics_data
    FOR SELECT
    USING (current_user IN (SELECT username FROM admins));

多个 Policy 之间是 OR 关系------满足任意一个即可访问。

超级用户绕过 RLS

这是一个非常重要的"坑":BYPASSRLS 属性的用户和超级用户(superuser)不受 RLS 限制。

sql 复制代码
-- 超级用户查询,RLS 不生效
SET ROLE postgres;
SELECT * FROM analytics_data;  -- 看到所有 4 行,不受 Policy 限制

如果你希望强制所有用户都受 RLS 约束(包括表 owner),需要加 FORCE

sql 复制代码
ALTER TABLE analytics_data FORCE ROW LEVEL SECURITY;

加了 FORCE 后,即使表 owner 也受 Policy 限制(但超级用户仍然绕过)。


三种常见的 Policy 模式

模式一:按用户隔离

最简单的场景------每个用户只能看自己的数据:

sql 复制代码
CREATE POLICY user_isolation ON analytics_data
    FOR ALL
    USING (user_id = current_user)
    WITH CHECK (user_id = current_user);

适合:个人数据管理、个人看板。

模式二:按租户隔离(多租户 SaaS)

SaaS 系统中,不同租户(公司/团队)的数据必须严格隔离。通常用一个 tenant_id 字段:

sql 复制代码
CREATE POLICY tenant_isolation ON analytics_data
    FOR ALL
    USING (tenant_id = current_setting('app.tenant_id')::INTEGER)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::INTEGER);

这里用到了 current_setting()------PostgreSQL 允许在会话中设置自定义变量:

sql 复制代码
-- 应用层在建立连接后设置当前租户
SET app.tenant_id = '42';

-- 之后所有查询自动过滤
SELECT * FROM analytics_data;  -- 只返回 tenant_id = 42 的行

适合:SaaS 平台、多组织系统。

模式三:按角色隔离

不同角色看到不同范围的数据:

sql 复制代码
-- 普通用户:只看自己的数据
CREATE POLICY user_view_own ON analytics_data
    FOR SELECT
    USING (user_id = current_user AND current_user NOT IN
           (SELECT username FROM role_assignments WHERE role = 'admin'));

-- admin:看所有数据
CREATE POLICY admin_view_all ON analytics_data
    FOR SELECT
    USING (current_user IN
           (SELECT username FROM role_assignments WHERE role = 'admin'));

适合:需要角色分级权限的系统。


为什么要在数据库层做,而不是应用层?

回到最初的困惑。研究完 RLS 后,我把应用层和数据库层做权限控制的区别整理如下:

维度 应用层过滤 RLS(数据库层)
安全性 低------遗漏一个 WHERE 就泄露 高------无法绕过,数据库引擎强制执行
覆盖范围 只覆盖经过应用的查询 覆盖所有数据库连接(包括直连工具)
维护成本 高------每个接口都要记得加条件 低------定义一次 Policy,全局生效
灵活性 高------可以写复杂业务逻辑 中------受限于 SQL 表达式
性能 取决于应用层实现 数据库引擎优化,索引可利用
适用场景 传统三层架构(应用 → DB) 直连数据库架构(如 Supabase / PostgREST)

核心区别在于一个词:默认安全(secure by default)

应用层过滤是"白名单"模式------你必须主动做对 每一处,漏一处就出事。RLS 是"黑名单"模式------默认就过滤好了,你必须主动绕过才能看到不该看的数据。从安全设计的角度,后者显然更可靠。

想象一下:应用层过滤就像在每条路上放一个检查站,你必须在每条路上都放对。RLS 则是给数据本身上了锁,不管你走哪条路,没钥匙就是看不到。

但 RLS 不是银弹

RLS 也有它的局限:

  1. 复杂业务逻辑不适合放进 Policy------比如"用户 A 可以看用户 B 的数据,但只能在工作时间、且 B 不在休假期间"。这种逻辑放 RLS 里会变成极度复杂的 SQL,难以维护
  2. 调试困难 ------RLS 是隐式生效的,出问题时很难一眼看出是哪个 Policy 导致的。你看到的 SELECT * 没返回数据,但不知道为什么
  3. 性能开销 ------每条查询都要评估 Policy 表达式,如果 Policy 里有子查询(如 current_user IN (SELECT ...)),可能影响性能
  4. Policy 冲突------多个 Policy 之间的 OR 关系可能导致意外的数据可见性

实践建议是:RLS 做粗粒度的安全隔离(用户/租户级别),应用层做细粒度的业务权限控制。 两者配合使用,而不是二选一。


Supabase 中的 RLS

Supabase 之所以大力推崇 RLS,是因为它的架构决定了前端直连数据库------通过 PostgREST,前端的 HTTP 请求直接转换为 SQL 查询打到 PostgreSQL。中间没有传统后端应用层。

这种架构下,RLS 是唯一的安全防线。如果不开 RLS,前端可以查询到表中所有数据------这比传统架构危险得多。

Supabase 在 RLS 之上还封装了一些便利机制:

  • auth.uid() :获取当前认证用户的 ID,比 current_user 更贴合应用层概念
  • Dashboard 可视化编辑 Policy:不用写 SQL,在管理界面点击配置
  • 模板化 Policy:提供常见模式的模板,一键应用
sql 复制代码
-- Supabase 中典型的 RLS Policy
CREATE POLICY "用户只能看自己的数据" ON analytics_data
    FOR SELECT
    USING (auth.uid()::text = user_id);

小结

研究完 RLS,最大的收获不是学会了几个 SQL 语法,而是安全思维方式的转变

  1. 应用层过滤是"主动做对",RLS 是"默认安全"------前者漏一处就泄露,后者默认就保护好了
  2. RLS 是数据库引擎层面的强制执行------不管你怎么查,都绕不过 Policy
  3. 直连数据库的架构(如 Supabase)让 RLS 从"可选"变成"必选"------没有应用层做中间防线,RLS 是唯一的安全屏障
  4. RLS 和应用层权限控制不是互斥的,而是互补的------RLS 做租户/用户级隔离,应用层做业务级权限

以前总觉得安全控制是应用层的事,数据库只管存数据。现在才意识到------最可靠的安全防线,应该放在离数据最近的地方。

相关推荐
用户298698530141 小时前
PDF 转图片?三种方案,覆盖全平台与自动化场景
人工智能·后端
沙湖遇雨1 小时前
2.启动客户端client
后端
程序员只因1 小时前
SpringBoot实战项目目录结构+高频注解详解(黑马点评适配版)
后端
Conan在掘金1 小时前
鸿蒙 7.0 小艺全面进化:超 200 项感知+全天候引擎+超强记忆——AI 私人助理根因
后端
老孙讲技术1 小时前
值班室画面慢半拍?5 分钟拿齐乐橙设备 HLS / FLV / 低延迟三条可播路径
后端·物联网
BingoGo2 小时前
PHP 引用计数机制深度解析
后端
人间凡尔赛2 小时前
2026 云原生“后容器时代“:WebAssembly 如何重构后端架构?
后端·云原生·架构
IT_陈寒2 小时前
React的useEffect依赖数组把我坑惨了,原来这样写才靠谱
前端·人工智能·后端
木易 士心2 小时前
服务器构建指南:从选型、部署到高可用架构详解
运维·服务器·后端·架构