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

2 阅读9分钟

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

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

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


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

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

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

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

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

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

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

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

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

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,数据库替你加。

最小示例

-- 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 身份查询:

-- alice 登录后执行
SELECT * FROM analytics_data;

结果:

 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 更智能——它会根据当前连接的用户身份动态生成。

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

两个关键子句:

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

一个表可以有多个 Policy

-- 策略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 限制。

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

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

ALTER TABLE analytics_data FORCE ROW LEVEL SECURITY;

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


三种常见的 Policy 模式

模式一:按用户隔离

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

CREATE POLICY user_isolation ON analytics_data
    FOR ALL
    USING (user_id = current_user)
    WITH CHECK (user_id = current_user);

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

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

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

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 允许在会话中设置自定义变量:

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

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

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

模式三:按角色隔离

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

-- 普通用户:只看自己的数据
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:提供常见模式的模板,一键应用
-- 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 做租户/用户级隔离,应用层做业务级权限

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