数据库

行级安全

使用Postgres行级安全保护您的数据。


当您需要细粒度的授权规则时,PostgreSQL的行级安全(RLS)是最佳选择。

Supabase中的行级安全

RLS功能极其强大且灵活,允许您编写符合独特业务需求的复杂SQL规则。RLS可以与Supabase认证结合使用,实现从浏览器到数据库的端到端用户安全。

RLS是Postgres的原生功能,可以提供"纵深防御",即使通过第三方工具访问也能保护您的数据免受恶意攻击。

策略

策略是Postgres的规则引擎。一旦掌握,策略就很容易理解。每个策略都附加到一个表上,每次访问表时都会执行该策略。

您可以将它们视为为每个查询添加一个WHERE子句。例如这样的策略...

1
2
3
create policy "个人可以查看自己的待办事项。"on todos for selectusing ( (select auth.uid()) = user_id );

...当用户尝试从todos表中选择时,会转换为这样:

1
2
3
4
select *from todoswhere auth.uid() = todos.user_id;-- 策略被隐式添加。

启用行级安全

您可以使用 enable row level security 子句为任何表启用行级安全(RLS):

1
alter table "table_name" enable row level security;

启用RLS后,当使用公共的anon密钥时,通过API将无法访问任何数据,直到您创建策略为止。

认证与非认证角色

Supabase将每个请求映射到以下角色之一:

  • anon:未认证请求(用户未登录)
  • authenticated:已认证请求(用户已登录)

这些实际上是Postgres角色。您可以在策略中使用TO子句来指定这些角色:

1
2
3
4
5
6
7
8
9
10
11
create policy "所有人都可查看个人资料"on profiles for selectto authenticated, anonusing ( true );-- 或者create policy "仅认证用户可查看公开资料"on profiles for selectto authenticatedusing ( true );

创建策略

策略是附加到Postgres表的SQL逻辑。您可以为每个表附加任意数量的策略。

如果您使用Supabase Auth,Supabase提供了一些辅助函数来简化RLS。我们将使用这些辅助函数来说明一些基本策略:

SELECT 查询策略

您可以使用 using 子句来指定 SELECT 查询策略。

假设您在 public 模式中有一个名为 profiles 的表,并且希望向所有人开放读取权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 1. 创建表create table profiles ( id uuid primary key, user_id references auth.users, avatar_url text);-- 2. 启用行级安全alter table profiles enable row level security;-- 3. 创建策略create policy "公开资料对所有人可见。"on profiles for selectto anon -- Postgres 角色(推荐)using ( true ); -- 实际策略

或者,如果您只想让用户查看自己的资料:

1
2
3
create policy "用户只能查看自己的资料。"on profilesfor select using ( (select auth.uid()) = user_id );

INSERT 插入策略

您可以使用 with check 子句来指定 INSERT 插入策略。with check 表达式确保任何新行数据都符合策略约束。

假设您在 public 模式中有一个名为 profiles 的表,并且只希望用户能够为自己创建资料。在这种情况下,我们需要检查他们的用户 ID 是否与尝试插入的值匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 1. 创建表create table profiles ( id uuid primary key, user_id uuid references auth.users, avatar_url text);-- 2. 启用行级安全alter table profiles enable row level security;-- 3. 创建策略create policy "用户可以创建自己的资料。"on profiles for insertto authenticated -- Postgres 角色(推荐)with check ( (select auth.uid()) = user_id ); -- 实际策略

UPDATE 策略

您可以通过组合 usingwith check 表达式来指定更新策略。

using 子句表示允许更新必须满足的条件,而 with check 子句确保所做的更新符合策略约束。

假设您在 public 模式中有一个名为 profiles 的表,并且只希望用户能够更新自己的个人资料。

您可以创建一个策略,其中 using 子句检查用户是否拥有正在更新的个人资料。而 with check 子句确保在结果行中,用户不会将 user_id 更改为不等于其用户 ID 的值,从而保持修改后的个人资料仍满足所有权条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 1. 创建表create table profiles ( id uuid primary key, user_id uuid references auth.users, avatar_url text);-- 2. 启用行级安全alter table profiles enable row level security;-- 3. 创建策略create policy "用户可更新自己的个人资料。"on profiles for updateto authenticated -- Postgres 角色(推荐)using ( (select auth.uid()) = user_id ) -- 检查现有行是否符合策略表达式with check ( (select auth.uid()) = user_id ); -- 检查新行是否符合策略表达式

如果未定义 with check 表达式,则 using 表达式将同时用于确定哪些行可见(正常的 USING 情况)和允许添加哪些新行(WITH CHECK 情况)。

DELETE 策略

您可以使用 using 子句来指定删除策略。

假设您在 public 模式中有一个名为 profiles 的表,并且只希望用户能够删除自己的个人资料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 1. 创建表create table profiles ( id uuid primary key, user_id uuid references auth.users, avatar_url text);-- 2. 启用行级安全(RLS)alter table profiles enable row level security;-- 3. 创建策略create policy "用户可删除自己的个人资料"on profiles for deleteto authenticated -- Postgres角色(推荐使用)using ( (select auth.uid()) = user_id ); -- 实际策略

视图

视图默认会绕过行级安全(RLS),因为它们通常由 postgres 用户创建。这是Postgres的一个特性,它会自动以 security definer 权限创建视图。

在Postgres 15及以上版本中,您可以通过设置 security_invoker = true 使视图在被 anonauthenticated 角色调用时遵守底层表的RLS策略。

1
2
3
create view <视图名称>with(security_invoker = true)as select <查询语句>

在较旧版本的Postgres中,可以通过撤销 anonauthenticated 角色的访问权限,或将视图放入未公开的模式中来保护视图。

辅助函数

Supabase提供了一些辅助函数,使编写策略更加方便。

auth.uid()

返回发起请求的用户ID。

auth.jwt()

返回发出请求的用户的JWT。您存储在用户raw_app_meta_data列或raw_user_meta_data列中的任何内容都可以通过此函数访问。了解这两者之间的区别很重要:

  • raw_user_meta_data - 可以通过经过身份验证的用户使用supabase.auth.update()函数更新。这不是存储授权数据的好地方。
  • raw_app_meta_data - 用户无法更新,因此是存储授权数据的好地方。

auth.jwt()函数非常灵活。例如,如果您在app_metadata中存储了一些团队数据,您可以使用它来确定特定用户是否属于某个团队。例如,如果这是一个ID数组:

1
2
3
4
create policy "用户属于团队"on my_tableto authenticatedusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));

多因素认证(MFA)

auth.jwt() 函数可用于检查多因素认证。例如,您可以限制用户更新个人资料,除非他们至少具备2级认证(保证等级2):

1
2
3
4
5
6
7
create policy "限制更新操作"on profilesas restrictivefor updateto authenticated using ( (select auth.jwt()->>'aal') = 'aal2');

绕过行级安全(RLS)

Supabase 提供了特殊的"服务"密钥,可用于绕过 RLS。这些密钥绝不应在浏览器中使用或暴露给客户,但对于管理任务非常有用。

您还可以创建新的 Postgres 角色,这些角色可以使用"bypass RLS"权限绕过行级安全:

1
alter role "角色名称" with bypassrls;

这对于系统级访问很有用。您应该_永远_不要共享具有此权限的任何 Postgres 角色的登录凭据。

RLS 性能优化建议

每个授权系统都会对性能产生影响。虽然行级安全功能强大,但性能影响需要特别注意。这对于扫描表中每一行的查询尤其重要 - 比如许多使用 limit、offset 和排序的 select 操作。

基于一系列测试,我们有以下 RLS 优化建议:

添加索引

请确保已为策略中使用的所有未建立索引(或非主键)的列添加索引。对于如下策略:

1
2
3
create policy "rls_test_select" on test_tableto authenticatedusing ( (select auth.uid()) = user_id );

您可以添加如下索引:

1
2
3
create index useridon test_tableusing btree (user_id);

基准测试

测试用例优化前(ms)优化后(ms)提升百分比变更详情
test1-indexed171< 0.199.94%
优化前:
无索引

优化后:
user_id 已建索引

使用 select 调用函数

您可以使用 select 语句来优化使用函数的策略。例如,替代以下写法:

1
2
3
create policy "rls_test_select" on test_tableto authenticatedusing ( auth.uid() = user_id );

可以采用:

1
2
3
create policy "rls_test_select" on test_tableto authenticatedusing ( (select auth.uid()) = user_id );

这种方法适用于 JWT 函数如 auth.uid()auth.jwt(),以及 security definer 函数。通过包装函数,Postgres 优化器会执行 initPlan,使其能够"缓存"每个语句的结果,而不是对每行数据都调用函数。

性能基准测试

测试用例优化前 (ms)优化后 (ms)提升百分比变更说明
test2a-wrappedSQL-uid179994.97%
优化前:
auth.uid() = user_id

优化后:
(select auth.uid()) = user_id
test2b-wrappedSQL-isadmin11,000799.94%
优化前:
is_admin() 表连接

优化后:
(select is_admin()) 表连接
test2c-wrappedSQL-two-functions11,0001099.91%
优化前:
is_admin() OR auth.uid() = user_id

优化后:
(select is_admin()) OR (select auth.uid() = user_id)
test2d-wrappedSQL-sd-fun178,0001299.993%
优化前:
has_role() = role

优化后:
(select has_role()) = role
test2e-wrappedSQL-sd-fun-array1730001699.991%
优化前:
team_id=any(user_teams())

优化后:
team_id=any(array(select user_teams()))

为每个查询添加过滤器

策略(Policies)是"隐式的where子句",因此常见的情况是在没有任何过滤器的情况下运行select语句。这对性能来说是一种不良模式。不要这样做(JS客户端示例):

1
2
3
const { data } = supabase .from('table') .select()

您应该始终添加一个过滤器:

1
2
3
4
const { data } = supabase .from('table') .select() .eq('user_id', userId)

尽管这会重复策略中的内容,但Postgres可以利用这个过滤器构建更好的查询计划。

基准测试

测试优化前(ms)优化后(ms)提升百分比变更说明
test3-addfilter171994.74%
优化前:
auth.uid() = user_id

优化后:
user_id上添加.eqwhere条件

使用安全定义者函数

"安全定义者"函数会使用_创建_该函数的角色权限来执行。这意味着如果您使用超级用户(如postgres)创建角色,那么该函数将拥有bypassrls权限。例如,如果您有这样的策略:

1
2
3
4
5
6
7
8
create policy "rls_test_select" on test_tableto authenticatedusing ( exists ( select 1 from roles_table where (select auth.uid()) = user_id and role = 'good_role' ));

我们可以改为创建一个security definer函数,该函数可以在不受任何RLS限制的情况下扫描roles_table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create function private.has_good_role()returns booleanlanguage plpgsqlsecurity definer -- 将以创建者身份运行as $$begin return exists ( select 1 from roles_table where (select auth.uid()) = user_id and role = 'good_role' );end;$$;-- 更新我们的策略以使用此函数:create policy "rls_test_select"on test_tableto authenticatedusing ( private.has_good_role() );

最小化连接操作

您通常可以重写策略(Policies)来避免源表和目标表之间的连接操作。相反,尝试组织您的策略,将目标表中的所有相关数据提取到数组或集合中,然后可以在过滤器中使用 INANY 操作。

例如,以下是一个性能较差的策略示例,它将源表 test_table 与目标表 team_user 进行连接:

1
2
3
4
5
6
7
8
9
create policy "rls_test_select" on test_tableto authenticatedusing ( (select auth.uid()) in ( select user_id from team_user where team_user.team_id = team_id -- 连接到源表"test_table.team_id" ));

我们可以重写这个策略来避免连接,改为将过滤条件选择到一个集合中:

1
2
3
4
5
6
7
8
9
create policy "rls_test_select" on test_tableto authenticatedusing ( team_id in ( select team_id from team_user where user_id = (select auth.uid()) -- 无连接操作 ));

在这种情况下,您也可以考虑使用 security definer 函数来绕过连接表的RLS限制:

性能基准测试

测试用例优化前(ms)优化后(ms)性能提升率变更说明
test5-fixed-join9,0002099.78%
优化前:
在表连接列中使用auth.uid()

优化后:
auth.uid()上连接表列

在策略中明确指定角色

始终在策略中使用TO操作符明确指定角色。例如,不要使用以下查询:

1
2
create policy "rls_test_select" on rls_testusing ( auth.uid() = user_id );

而应该使用:

1
2
3
create policy "rls_test_select" on rls_testto authenticatedusing ( (select auth.uid()) = user_id );

这样可以防止策略( (select auth.uid()) = user_id )对任何anon用户执行,因为执行会在to authenticated步骤停止。

性能基准测试

测试案例优化前 (ms)优化后 (ms)性能提升率变更说明
test6-To-role170< 0.199.78%
优化前:
TO 策略

优化后:
TO authenticated (匿名用户访问)

更多资源