数据库

行级别安全

使用 Postgres 行级别安全性来保护你的数据。


当你需要细粒度的授权规则时,没有比 Postgres 的 行级别安全性 (RLS) 更好的选择了。

Supabase 中的行级别安全性#

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

RLS 是 Postgres 的基本功能,即使通过第三方工具访问,也能提供 "纵深防御" 来保护你的数据,防止恶意攻击者。

策略#

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

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

1
create policy "Individuals can view their own todos."
2
on todos for select
3
using ( (select auth.uid()) = user_id );

.. 当用户尝试从 todos 表中选择数据时,将会转换为以下内容

1
select *
2
from todos
3
where auth.uid() = todos.user_id;
4
-- Policy is implicitly added.

启用行级别安全性#

你可以使用 enable row level security 子句为任何表启用 RLS

1
alter table "table_name" enable row level security;

一旦你启用了 RLS,在创建策略之前,将无法通过 API 使用公共 anon 密钥访问任何数据。

为新表自动启用 RLS#

如果你希望为新表自动启用 RLS,你可以创建一个事件触发器,在表创建后运行。这使用 Postgres 事件触发器 来调用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY,从而在新创建的每个表上启用 RLS。

1
CREATE OR REPLACE FUNCTION rls_auto_enable()
2
RETURNS EVENT_TRIGGER
3
LANGUAGE plpgsql
4
SECURITY DEFINER
5
SET search_path = pg_catalog
6
AS $$
7
DECLARE
8
cmd record;
9
BEGIN
10
FOR cmd IN
11
SELECT *
12
FROM pg_event_trigger_ddl_commands()
13
WHERE command_tag IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
14
AND object_type IN ('table','partitioned table')
15
LOOP
16
IF cmd.schema_name IS NOT NULL AND cmd.schema_name IN ('public') AND cmd.schema_name NOT IN ('pg_catalog','information_schema') AND cmd.schema_name NOT LIKE 'pg_toast%' AND cmd.schema_name NOT LIKE 'pg_temp%' THEN
17
BEGIN
18
EXECUTE format('alter table if exists %s enable row level security', cmd.object_identity);
19
RAISE LOG 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
20
EXCEPTION
21
WHEN OTHERS THEN
22
RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
23
END;
24
ELSE
25
RAISE LOG 'rls_auto_enable: skip % (either system schema or not in enforced list: %.)', cmd.object_identity, cmd.schema_name;
26
END IF;
27
END LOOP;
28
END;
29
$$;
30
31
DROP EVENT TRIGGER IF EXISTS ensure_rls;
32
CREATE EVENT TRIGGER ensure_rls
33
ON ddl_command_end
34
WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
35
EXECUTE FUNCTION rls_auto_enable();

请注意,这适用于在安装触发器后创建的表。现有表仍然需要手动启用 RLS。

已认证和未认证的角色#

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

  • anon:未经验证的请求(用户未登录)
  • authenticated:已经验证的请求(用户已登录)

这些实际上是 Postgres 角色。你可以使用 TO 子句在你的策略中使用这些角色

1
create policy "Profiles are viewable by everyone"
2
on profiles for select
3
to authenticated, anon
4
using ( true );
5
6
-- OR
7
8
create policy "Public profiles are viewable only by authenticated users"
9
on profiles for select
10
to authenticated
11
using ( true );

创建策略#

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

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

SELECT 策略#

你可以使用 using 子句指定 SELECT 策略。

假设你有一个名为 profiles 的表在 public schema 中,并且你希望启用对所有人的读取访问权限。

1
-- 1. Create table
2
create table profiles (
3
id uuid primary key,
4
user_id uuid references auth.users,
5
avatar_url text
6
);
7
8
-- 2. Enable RLS
9
alter table profiles enable row level security;
10
11
-- 3. Create Policy
12
create policy "Public profiles are visible to everyone."
13
on profiles for select
14
to anon -- the Postgres Role (recommended)
15
using ( true ); -- the actual Policy

或者,如果你只想让用户能够看到他们自己的个人资料

1
create policy "User can see their own profile only."
2
on profiles
3
for select using ( (select auth.uid()) = user_id );

INSERT 策略#

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

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

1
-- 1. Create table
2
create table profiles (
3
id uuid primary key,
4
user_id uuid references auth.users,
5
avatar_url text
6
);
7
8
-- 2. Enable RLS
9
alter table profiles enable row level security;
10
11
-- 3. Create Policy
12
create policy "Users can create a profile."
13
on profiles for insert
14
to authenticated -- the Postgres Role (recommended)
15
with check ( (select auth.uid()) = user_id ); -- the actual Policy

UPDATE 策略#

你可以通过组合 usingwith check 表达式来指定 UPDATE 策略。

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

假设你有一个名为 profiles 的表在 public schema 中,并且你只想让用户能够更新他们自己的个人资料。

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

1
-- 1. Create table
2
create table profiles (
3
id uuid primary key,
4
user_id uuid references auth.users,
5
avatar_url text
6
);
7
8
-- 2. Enable RLS
9
alter table profiles enable row level security;
10
11
-- 3. Create Policy
12
create policy "Users can update their own profile."
13
on profiles for update
14
to authenticated -- the Postgres Role (recommended)
15
using ( (select auth.uid()) = user_id ) -- checks if the existing row complies with the policy expression
16
with check ( (select auth.uid()) = user_id ); -- checks if the new row complies with the policy expression

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

DELETE 策略#

你可以使用 using 子句指定 DELETE 策略。

假设你有一个名为 profiles 的表在 public schema 中,并且你只想让用户能够删除他们自己的个人资料

1
-- 1. Create table
2
create table profiles (
3
id uuid primary key,
4
user_id uuid references auth.users,
5
avatar_url text
6
);
7
8
-- 2. Enable RLS
9
alter table profiles enable row level security;
10
11
-- 3. Create Policy
12
create policy "Users can delete a profile."
13
on profiles for delete
14
to authenticated -- the Postgres Role (recommended)
15
using ( (select auth.uid()) = user_id ); -- the actual Policy

视图#

由于它们通常使用 postgres 用户创建,因此视图默认绕过 RLS。这是 Postgres 的一项功能,它会自动使用 security definer 创建视图。

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

1
create view <VIEW_NAME>
2
with(security_invoker = true)
3
as select <QUERY>

在旧版本的 Postgres 中,通过从 anonauthenticated 角色撤销访问权限,或将它们放在未公开的 schema 中来保护你的视图。

助手函数#

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
create policy "User is in team"
2
on my_table
3
to authenticated
4
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));

MFA#

可以使用 auth.jwt() 函数来检查 多因素身份验证。例如,你可以限制用户在没有至少 2 级身份验证(保证级别 2)的情况下更新他们的个人资料

1
create policy "Restrict updates."
2
on profiles
3
as restrictive
4
for update
5
to authenticated using (
6
(select auth.jwt()->>'aal') = 'aal2'
7
);

绕过行级别安全性#

Supabase 提供特殊的“服务”密钥,可用于绕过 RLS。这些绝不能在浏览器中使用或暴露给客户,但它们对于管理任务很有用。

你还可以创建新的 Postgres 角色,这些角色可以使用“绕过 RLS”权限来绕过行级别安全性

1
alter role "role_name" with bypassrls;

这对于系统级访问很有用。你绝不 应该与任何具有此权限的 Postgres 角色的登录凭据共享。

RLS 性能建议#

每个授权系统都会对性能产生影响。虽然行级别安全性功能强大,但性能影响很重要。这对于扫描表中每一行的查询(例如,许多 select 操作,包括那些使用 limit、offset 和 ordering 的操作)尤其如此。

基于一系列 测试,我们有一些关于 RLS 的建议

添加索引#

确保你已在策略中使用的任何未索引的列(或主键)上添加 索引。对于像这样的策略

1
create policy "rls_test_select" on test_table
2
to authenticated
3
using ( (select auth.uid()) = user_id );

你可以添加一个索引,例如

1
create index userid
2
on test_table
3
using btree (user_id);

基准测试#

测试之前 (ms)之后 (ms)% 提升变更
test1-indexed171< 0.199.94%
之前
没有索引

之后
user_id 已索引

使用 select 调用函数#

您可以使用 select 语句来改进使用函数的策略。例如,不要使用以下方式

1
create policy "rls_test_select" on test_table
2
to authenticated
3
using ( auth.uid() = user_id );

您可以这样做

1
create policy "rls_test_select" on test_table
2
to authenticated
3
using ( (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()))

为每个查询添加筛选器#

策略是“隐式 where 子句”,因此通常会运行不带任何筛选器的 select 语句。这对性能来说是一个坏模式。不要这样做(JS 客户端示例)

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

您应该始终添加一个筛选器

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

即使这会复制策略的内容,Postgres 也可以使用筛选器来构建更好的查询计划。

基准测试#

测试之前 (ms)之后 (ms)% 提升变更
test3-addfilter171994.74%
之前
auth.uid() = user_id

之后
添加 .eqwhereuser_id

使用 security definer 函数#

“security definer” 函数使用创建该函数的相同角色运行。这意味着,如果您创建一个具有超级用户权限的角色(例如 postgres),那么该函数将具有 bypassrls 权限。例如,如果您有一个像这样的策略

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

我们可以创建一个 security definer 函数,它可以扫描 roles_table 而没有任何 RLS 处罚

1
create function private.has_good_role()
2
returns boolean
3
language plpgsql
4
security definer -- will run as the creator
5
as $$
6
begin
7
return exists (
8
select 1 from roles_table
9
where (select auth.uid()) = user_id and role = 'good_role'
10
);
11
end;
12
$$;
13
14
-- Update our policy to use this function:
15
create policy "rls_test_select"
16
on test_table
17
to authenticated
18
using ( (select private.has_good_role()) );

最小化连接#

您通常可以重写您的策略以避免源表和目标表之间的连接。相反,尝试将您的策略组织为将目标表中的所有相关数据获取到一个数组或集合中,然后您可以在筛选器中使用 INANY 操作。

例如,这是一个缓慢策略的示例,它将源 test_table 连接到目标 team_user

1
create policy "rls_test_select" on test_table
2
to authenticated
3
using (
4
(select auth.uid()) in (
5
select user_id
6
from team_user
7
where team_user.team_id = team_id -- joins to the source "test_table.team_id"
8
)
9
);

我们可以重写它以避免此连接,而是将筛选条件选择到一个集合中

1
create policy "rls_test_select" on test_table
2
to authenticated
3
using (
4
team_id in (
5
select team_id
6
from team_user
7
where user_id = (select auth.uid()) -- no join
8
)
9
);

在这种情况下,您还可以考虑 使用 security definer 函数 来绕过连接表上的 RLS。

基准测试#

测试之前 (ms)之后 (ms)% 提升变更
test5-fixed-join9,0002099.78%
之前
auth.uid() 在 col 上的表连接

之后
col 在 auth.uid() 上的表连接

在您的策略中指定角色#

始终在您的策略中使用 Role,由 TO 运算符指定。例如,不要使用此查询

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

用途

1
create policy "rls_test_select" on rls_test
2
to authenticated
3
using ( (select auth.uid()) = user_id );

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

基准测试#

测试之前 (ms)之后 (ms)% 提升变更
test6-To-role170< 0.199.78%
之前
没有 TO 策略

之后
TO authenticated (anon 访问)

更多资源#