最近在做一个数据分析看板的需求,技术选型时考虑用 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:作用于SELECT、UPDATE、DELETE——决定用户能看到/操作哪些行WITH CHECK:作用于INSERT、UPDATE——决定用户能写入哪些行(写入后必须满足此条件)
一个表可以有多个 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 也有它的局限:
- 复杂业务逻辑不适合放进 Policy——比如"用户 A 可以看用户 B 的数据,但只能在工作时间、且 B 不在休假期间"。这种逻辑放 RLS 里会变成极度复杂的 SQL,难以维护
- 调试困难——RLS 是隐式生效的,出问题时很难一眼看出是哪个 Policy 导致的。你看到的
SELECT *没返回数据,但不知道为什么 - 性能开销——每条查询都要评估 Policy 表达式,如果 Policy 里有子查询(如
current_user IN (SELECT ...)),可能影响性能 - 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 语法,而是安全思维方式的转变:
- 应用层过滤是"主动做对",RLS 是"默认安全"——前者漏一处就泄露,后者默认就保护好了
- RLS 是数据库引擎层面的强制执行——不管你怎么查,都绕不过 Policy
- 直连数据库的架构(如 Supabase)让 RLS 从"可选"变成"必选"——没有应用层做中间防线,RLS 是唯一的安全屏障
- RLS 和应用层权限控制不是互斥的,而是互补的——RLS 做租户/用户级隔离,应用层做业务级权限
以前总觉得安全控制是应用层的事,数据库只管存数据。现在才意识到——最可靠的安全防线,应该放在离数据最近的地方。