跳到主要内容

基础权限

CloudBase PostgreSQL 数据库通过 行级安全策略(Row Level Security,RLS) 实现基础数据权限控制。

RLS 允许你在数据库层面定义访问策略,精确控制每一行数据的读写权限,确保用户只能访问被授权的数据。

RLS 机制简介

PostgreSQL 的 RLS 机制通过在表上创建安全策略(Policy)来控制行级别的数据访问。其核心流程为:

  1. 启用 RLS:对目标表开启行级安全策略
  2. 创建策略:定义允许用户执行哪些操作(SELECT、INSERT、UPDATE、DELETE)以及可访问哪些行
  3. 自动过滤:数据库在执行查询时,自动根据策略过滤数据行,对应用层完全透明

在 CloudBase 中,通过 auth.uid() 获取当前请求用户的身份标识,你可以在策略中将其与表中自定义的用户标识字段进行比较,实现数据归属控制。

CloudBase 为用户分配了以下数据库角色:

  • anon:未登录用户拥有的角色
  • authenticated:登录用户拥有的角色

你可以在 RLS 策略中结合角色和 auth.uid() 来实现更灵活的权限控制。

配置方式

通过执行 SQL 语句来配置表的权限策略。以下是几种常见场景的 Policy 创建示例。

提示

示例中使用 user_id 作为数据归属字段名,你可以根据实际表结构替换为其他字段。

读取全部数据,修改本人数据

适用于用户评论、用户公开信息等场景——登录用户可查看全部数据,但只有数据拥有者可以修改自己的数据。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 允许登录用户读取全部数据
CREATE POLICY select_all ON my_table
FOR SELECT
TO authenticated
USING (true);

-- 仅允许用户修改自己的数据
CREATE POLICY update_own ON my_table
FOR UPDATE
TO authenticated
USING (user_id = (select auth.uid()))
WITH CHECK (user_id = (select auth.uid()));

-- 仅允许用户删除自己的数据
CREATE POLICY delete_own ON my_table
FOR DELETE
TO authenticated
USING (user_id = (select auth.uid()));

-- 插入时自动绑定数据归属
CREATE POLICY insert_own ON my_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

读取和修改本人数据

适用于用户个人设置、用户订单管理等场景——用户只能查看和操作自己的数据。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 仅允许用户读取自己的数据
CREATE POLICY select_own ON my_table
FOR SELECT
TO authenticated
USING (user_id = (select auth.uid()));

-- 仅允许用户修改自己的数据
CREATE POLICY update_own ON my_table
FOR UPDATE
TO authenticated
USING (user_id = (select auth.uid()))
WITH CHECK (user_id = (select auth.uid()));

-- 仅允许用户删除自己的数据
CREATE POLICY delete_own ON my_table
FOR DELETE
TO authenticated
USING (user_id = (select auth.uid()));

-- 插入时自动绑定数据归属
CREATE POLICY insert_own ON my_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

所有人可读数据(含未登录用户)

适用于公告信息、帮助文档等场景——任何人(包括未登录用户)都可以查看数据,无需登录。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 允许所有人(含未登录用户)读取数据
CREATE POLICY select_anon ON my_table
FOR SELECT
TO anon, authenticated
USING (true);

读取全部数据,不可修改数据

适用于商品信息、系统配置等场景——登录用户可查看全部数据,但不允许通过客户端修改。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 允许登录用户读取全部数据
CREATE POLICY select_all ON my_table
FOR SELECT
TO authenticated
USING (true);

-- 不创建 INSERT / UPDATE / DELETE 策略
-- RLS 启用后,未被策略允许的操作默认被拒绝

基于状态的条件访问

适用于文章发布、商品上下架等场景——登录用户可查看所有已发布的数据,作者还可以查看和修改自己的草稿。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 登录用户可查看已发布数据,或自己的草稿
CREATE POLICY select_published_or_own ON my_table
FOR SELECT
TO authenticated
USING (status = 'published' OR user_id = (select auth.uid()));

-- 仅允许用户修改自己的数据
CREATE POLICY update_own ON my_table
FOR UPDATE
TO authenticated
USING (user_id = (select auth.uid()))
WITH CHECK (user_id = (select auth.uid()));

-- 仅允许用户删除自己的数据
CREATE POLICY delete_own ON my_table
FOR DELETE
TO authenticated
USING (user_id = (select auth.uid()));

-- 插入时自动绑定数据归属
CREATE POLICY insert_own ON my_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

团队共享数据

适用于多租户、团队协作等场景——同一团队的成员可以互相查看和操作数据。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 允许用户查看同一团队的数据
CREATE POLICY select_team ON my_table
FOR SELECT
TO authenticated
USING (team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid())));

-- 允许用户修改同一团队的数据
CREATE POLICY update_team ON my_table
FOR UPDATE
TO authenticated
USING (team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid())))
WITH CHECK (team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid())));

-- 允许用户在所属团队中插入数据
CREATE POLICY insert_team ON my_table
FOR INSERT
TO authenticated
WITH CHECK (team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid())));

-- 允许用户删除同一团队的数据
CREATE POLICY delete_team ON my_table
FOR DELETE
TO authenticated
USING (team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid())));

仅允许插入,不可修改删除

适用于用户反馈、操作日志等场景——登录用户可以提交数据,但提交后不可修改或删除。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 允许登录用户查看自己提交的数据
CREATE POLICY select_own ON my_table
FOR SELECT
TO authenticated
USING (user_id = (select auth.uid()));

-- 允许登录用户插入数据
CREATE POLICY insert_own ON my_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

-- 不创建 UPDATE / DELETE 策略
-- 数据提交后不可修改或删除

无权限

适用于后台流水数据、内部审计日志等场景——客户端用户不可直接访问,仅允许通过云函数等服务端逻辑操作。

-- 启用 RLS
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;

-- 不创建任何策略
-- RLS 启用后,所有客户端操作默认被拒绝
提示

对于"无权限"的表,你仍然可以通过云函数等服务端方式访问数据,服务端使用管理员权限不受 RLS 限制。

最佳实践

使用 DEFAULT 自动填充 user_id

建表时为 user_id 字段设置 DEFAULT auth.uid(),插入数据时数据库会自动填充当前用户的身份标识,无需客户端传值,避免伪造风险:

CREATE TABLE my_table (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id VARCHAR(64) NOT NULL DEFAULT auth.uid(),
content TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

这样客户端插入数据时无需指定 user_id,数据库会自动绑定当前登录用户:

// 无需传 user_id,数据库自动填充
const { error } = await db
.from("my_table")
.insert({ content: "hello world" });

配合 RLS 防止篡改

即使设置了 DEFAULT,恶意客户端仍可能在插入时显式传入伪造的 user_id。配合 INSERT 策略的 WITH CHECK 约束,可以确保写入的 user_id 必须与当前登录用户一致:

CREATE POLICY insert_own ON my_table
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

当客户端尝试传入他人的 user_id 时,该策略会拒绝写入。

理解 USING 与 WITH CHECK 的区别

在 RLS 策略中,USINGWITH CHECK 扮演不同的角色:

  • USING:控制用户能读取或操作哪些现有行。用于 SELECT、UPDATE(筛选可修改的行)、DELETE。
  • WITH CHECK:控制用户写入的新数据必须满足什么条件。用于 INSERT 和 UPDATE(验证修改后的行)。

对于 UPDATE 操作,两者配合使用尤为重要:

CREATE POLICY update_own ON my_table
FOR UPDATE
TO authenticated
-- USING:只能修改自己的数据
USING (user_id = (select auth.uid()))
-- WITH CHECK:修改后 user_id 仍然必须是自己
WITH CHECK (user_id = (select auth.uid()));

如果 UPDATE 策略只写了 USING 而没有 WITH CHECK,用户虽然只能修改自己的行,但可能将 user_id 改为其他人的值,导致数据归属被篡改。加上 WITH CHECK 后,数据库会验证修改后的行仍然满足条件,否则拒绝操作。

注意

所有 UPDATE 策略都建议同时包含 USINGWITH CHECK,以确保数据在修改前后都满足权限约束。

auth.uid() 的 null 行为

当用户未登录时,auth.uid() 返回 null。由于 SQL 中 null = any_value 的结果永远为 false(而非 truenull),因此基于 auth.uid() 的策略在用户未登录时会自动拒绝访问,这是安全的。

但需要注意以下场景:

  • 如果你的策略使用 != 比较(如 user_id != (select auth.uid())),null != value 也返回 false,可能不符合预期
  • 如果需要明确区分"未登录"和"登录但不匹配"两种情况,可以加上 auth.uid() IS NOT NULL 检查
-- 明确要求用户已登录,且数据属于自己
CREATE POLICY select_own ON my_table
FOR SELECT
TO authenticated
USING (auth.uid() IS NOT NULL AND user_id = (select auth.uid()));

View 与 RLS 的交互

PostgreSQL 中的视图(View)默认以创建者权限security_definer)执行,这意味着通过 View 查询数据时,RLS 策略不会生效,可能导致数据泄露。

在 PostgreSQL 15 及以上版本中,可以使用 security_invoker 选项让 View 以调用者权限执行,从而使 RLS 策略正常生效:

-- 创建安全的 View(PostgreSQL 15+)
CREATE VIEW public_posts
WITH (security_invoker = true)
AS SELECT id, title, content, user_id FROM posts;
注意

如果你使用了 View 且表上启用了 RLS,请务必确认 View 的安全模式。未设置 security_invoker = true 的 View 会绕过 RLS,导致所有用户都能通过该 View 访问全部数据。

性能优化

RLS 策略在每次查询时都会被执行,不当的写法可能严重影响查询性能。以下是关键的优化建议。

使用子查询缓存 auth.uid()

在 RLS 策略中,直接调用 auth.uid() 会导致 PostgreSQL 对结果集的每一行都调用一次该函数。使用 (select auth.uid()) 将其包裹为子查询后,PostgreSQL 会将其识别为常量,仅计算一次

-- ❌ 未优化:对每一行都调用 auth.uid()
CREATE POLICY select_own ON my_table
FOR SELECT TO authenticated
USING (user_id = auth.uid());

-- ✅ 推荐:使用子查询,仅计算一次
CREATE POLICY select_own ON my_table
FOR SELECT TO authenticated
USING (user_id = (select auth.uid()));

在大数据量场景下,这一优化可以将查询性能提升数个数量级。

提示

本文档中的所有示例均已使用优化写法。在编写自定义策略时,请始终使用 (select auth.uid()) 而非 auth.uid()

为策略字段创建索引

RLS 策略中用于过滤的字段(如 user_idteam_id)应创建索引,否则每次查询都需要全表扫描:

-- 为常用的策略字段创建索引
CREATE INDEX idx_my_table_user_id ON my_table (user_id);
CREATE INDEX idx_my_table_team_id ON my_table (team_id);

对于复合条件的策略,可以创建复合索引:

-- 适用于"基于状态的条件访问"场景
CREATE INDEX idx_my_table_status_user ON my_table (status, user_id);

查询时添加显式过滤条件

即使已经配置了 RLS 策略,在客户端查询时仍建议添加显式的过滤条件。这有助于 PostgreSQL 查询优化器生成更高效的执行计划:

// ❌ 仅依赖 RLS 过滤
const { data } = await db
.from("my_table")
.select();

// ✅ 添加显式过滤,帮助优化器
const { data } = await db
.from("my_table")
.select()
.eq("user_id", userId);

RLS 策略在查询计划层面充当安全保障,而显式过滤条件可以让优化器更早地缩小扫描范围,两者配合使用效果最佳。