行级安全
使用Postgres行级安全保护您的数据。
当您需要细粒度的授权规则时,PostgreSQL的行级安全(RLS)是最佳选择。
Supabase中的行级安全
只要启用了RLS,Supabase就能从浏览器提供便捷且安全的数据访问。
在任何暴露模式中的表上都必须启用RLS。默认情况下,这指的是public
模式。
通过仪表板中的表编辑器创建的表默认会启用RLS。如果您使用原始SQL或SQL编辑器创建表,请记得手动启用RLS:
12alter table <schema_name>.<table_name>enable row level security;
RLS功能极其强大且灵活,允许您编写符合独特业务需求的复杂SQL规则。RLS可以与Supabase认证结合使用,实现从浏览器到数据库的端到端用户安全。
RLS是Postgres的原生功能,可以提供"纵深防御",即使通过第三方工具访问也能保护您的数据免受恶意攻击。
策略
策略是Postgres的规则引擎。一旦掌握,策略就很容易理解。每个策略都附加到一个表上,每次访问表时都会执行该策略。
您可以将它们视为为每个查询添加一个WHERE
子句。例如这样的策略...
123create policy "个人可以查看自己的待办事项。"on todos for selectusing ( (select auth.uid()) = user_id );
...当用户尝试从todos表中选择时,会转换为这样:
1234select *from todoswhere auth.uid() = todos.user_id;-- 策略被隐式添加。
启用行级安全
您可以使用 enable row level security
子句为任何表启用行级安全(RLS):
1alter table "table_name" enable row level security;
启用RLS后,当使用公共的anon
密钥时,通过API将无法访问任何数据,直到您创建策略为止。
认证与非认证角色
Supabase将每个请求映射到以下角色之一:
anon
:未认证请求(用户未登录)authenticated
:已认证请求(用户已登录)
这些实际上是Postgres角色。您可以在策略中使用TO
子句来指定这些角色:
1234567891011create policy "所有人都可查看个人资料"on profiles for selectto authenticated, anonusing ( true );-- 或者create policy "仅认证用户可查看公开资料"on profiles for selectto authenticatedusing ( true );
匿名用户与anon密钥的区别
使用anon
Postgres角色与Supabase Auth中的匿名用户不同。匿名用户在访问数据库时采用authenticated
角色,可以通过检查JWT中的is_anonymous
声明来与永久用户区分。
创建策略
策略是附加到Postgres表的SQL逻辑。您可以为每个表附加任意数量的策略。
如果您使用Supabase Auth,Supabase提供了一些辅助函数来简化RLS。我们将使用这些辅助函数来说明一些基本策略:
SELECT 查询策略
您可以使用 using
子句来指定 SELECT 查询策略。
假设您在 public 模式中有一个名为 profiles
的表,并且希望向所有人开放读取权限。
123456789101112131415-- 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 ); -- 实际策略
或者,如果您只想让用户查看自己的资料:
123create policy "用户只能查看自己的资料。"on profilesfor select using ( (select auth.uid()) = user_id );
INSERT 插入策略
您可以使用 with check
子句来指定 INSERT 插入策略。with check
表达式确保任何新行数据都符合策略约束。
假设您在 public 模式中有一个名为 profiles
的表,并且只希望用户能够为自己创建资料。在这种情况下,我们需要检查他们的用户 ID 是否与尝试插入的值匹配:
123456789101112131415-- 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 策略
您可以通过组合 using
和 with check
表达式来指定更新策略。
using
子句表示允许更新必须满足的条件,而 with check
子句确保所做的更新符合策略约束。
假设您在 public 模式中有一个名为 profiles
的表,并且只希望用户能够更新自己的个人资料。
您可以创建一个策略,其中 using
子句检查用户是否拥有正在更新的个人资料。而 with check
子句确保在结果行中,用户不会将 user_id
更改为不等于其用户 ID 的值,从而保持修改后的个人资料仍满足所有权条件。
12345678910111213141516-- 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 情况)。
执行 UPDATE
操作需要对应的 SELECT
策略。如果没有 SELECT
策略,UPDATE
操作将无法按预期工作。
DELETE 策略
您可以使用 using
子句来指定删除策略。
假设您在 public 模式中有一个名为 profiles
的表,并且只希望用户能够删除自己的个人资料:
123456789101112131415-- 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
使视图在被 anon
和 authenticated
角色调用时遵守底层表的RLS策略。
123create view <视图名称>with(security_invoker = true)as select <查询语句>
在较旧版本的Postgres中,可以通过撤销 anon
和 authenticated
角色的访问权限,或将视图放入未公开的模式中来保护视图。
辅助函数
Supabase提供了一些辅助函数,使编写策略更加方便。
auth.uid()
返回发起请求的用户ID。
auth.jwt()
注意:不应在RLS策略中使用JWT中的所有信息。例如,创建一个依赖user_metadata
声明的RLS策略可能会在您的应用程序中产生安全问题,因为这些信息可以被经过身份验证的终端用户修改。
返回发出请求的用户的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数组:
1234create policy "用户属于团队"on my_tableto authenticatedusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
请注意,JWT并不总是"最新"的。在上面的例子中,即使您从团队中移除了用户并更新了app_metadata
字段,这些更改在使用auth.jwt()
时也不会反映出来,直到用户的JWT被刷新。
此外,如果您使用Cookie进行身份验证,则必须注意JWT的大小。某些浏览器对每个Cookie的大小限制为4096字节,因此您的JWT总大小应足够小以适应此限制。
多因素认证(MFA)
auth.jwt()
函数可用于检查多因素认证。例如,您可以限制用户更新个人资料,除非他们至少具备2级认证(保证等级2):
1234567create policy "限制更新操作"on profilesas restrictivefor updateto authenticated using ( (select auth.jwt()->>'aal') = 'aal2');
绕过行级安全(RLS)
Supabase 提供了特殊的"服务"密钥,可用于绕过 RLS。这些密钥绝不应在浏览器中使用或暴露给客户,但对于管理任务非常有用。
即使客户端库使用服务密钥初始化,Supabase 仍会遵守已登录用户的 RLS 策略。
您还可以创建新的 Postgres 角色,这些角色可以使用"bypass RLS"权限绕过行级安全:
1alter role "角色名称" with bypassrls;
这对于系统级访问很有用。您应该_永远_不要共享具有此权限的任何 Postgres 角色的登录凭据。
RLS 性能优化建议
每个授权系统都会对性能产生影响。虽然行级安全功能强大,但性能影响需要特别注意。这对于扫描表中每一行的查询尤其重要 - 比如许多使用 limit、offset 和排序的 select
操作。
基于一系列测试,我们有以下 RLS 优化建议:
添加索引
请确保已为策略中使用的所有未建立索引(或非主键)的列添加索引。对于如下策略:
123create policy "rls_test_select" on test_tableto authenticatedusing ( (select auth.uid()) = user_id );
您可以添加如下索引:
123create index useridon test_tableusing btree (user_id);
基准测试
测试用例 | 优化前(ms) | 优化后(ms) | 提升百分比 | 变更详情 |
---|---|---|---|---|
test1-indexed | 171 | < 0.1 | 99.94% | 优化前: 无索引 优化后: user_id 已建索引 |
使用 select
调用函数
您可以使用 select
语句来优化使用函数的策略。例如,替代以下写法:
123create policy "rls_test_select" on test_tableto authenticatedusing ( auth.uid() = user_id );
可以采用:
123create 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-uid | 179 | 9 | 94.97% | 优化前: auth.uid() = user_id 优化后: (select auth.uid()) = user_id |
test2b-wrappedSQL-isadmin | 11,000 | 7 | 99.94% | 优化前: is_admin() 表连接优化后: (select is_admin()) 表连接 |
test2c-wrappedSQL-two-functions | 11,000 | 10 | 99.91% | 优化前: is_admin() OR auth.uid() = user_id 优化后: (select is_admin()) OR (select auth.uid() = user_id) |
test2d-wrappedSQL-sd-fun | 178,000 | 12 | 99.993% | 优化前: has_role() = role 优化后: (select has_role()) = role |
test2e-wrappedSQL-sd-fun-array | 173000 | 16 | 99.991% | 优化前: team_id=any(user_teams()) 优化后: team_id=any(array(select user_teams())) |
为每个查询添加过滤器
策略(Policies)是"隐式的where子句",因此常见的情况是在没有任何过滤器的情况下运行select
语句。这对性能来说是一种不良模式。不要这样做(JS客户端示例):
123const { data } = supabase .from('table') .select()
您应该始终添加一个过滤器:
1234const { data } = supabase .from('table') .select() .eq('user_id', userId)
尽管这会重复策略中的内容,但Postgres可以利用这个过滤器构建更好的查询计划。
基准测试
测试 | 优化前(ms) | 优化后(ms) | 提升百分比 | 变更说明 |
---|---|---|---|---|
test3-addfilter | 171 | 9 | 94.74% | 优化前: auth.uid() = user_id 优化后: 在 user_id 上添加.eq 或where 条件 |
使用安全定义者函数
"安全定义者"函数会使用_创建_该函数的角色权限来执行。这意味着如果您使用超级用户(如postgres
)创建角色,那么该函数将拥有bypassrls
权限。例如,如果您有这样的策略:
12345678create 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
:
123456789101112131415161718create 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() );
安全定义者函数绝不应该创建在您API设置中"公开模式"下的任何模式中。
最小化连接操作
您通常可以重写策略(Policies)来避免源表和目标表之间的连接操作。相反,尝试组织您的策略,将目标表中的所有相关数据提取到数组或集合中,然后可以在过滤器中使用 IN
或 ANY
操作。
例如,以下是一个性能较差的策略示例,它将源表 test_table
与目标表 team_user
进行连接:
123456789create 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" ));
我们可以重写这个策略来避免连接,改为将过滤条件选择到一个集合中:
123456789create 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限制:
如果列表超过1000个项目,可能需要采用不同的方法,或者需要分析该方法以确保性能可接受。
性能基准测试
测试用例 | 优化前(ms) | 优化后(ms) | 性能提升率 | 变更说明 |
---|---|---|---|---|
test5-fixed-join | 9,000 | 20 | 99.78% | 优化前: 在表连接列中使用 auth.uid() 优化后: 在 auth.uid() 上连接表列 |
在策略中明确指定角色
始终在策略中使用TO
操作符明确指定角色。例如,不要使用以下查询:
12create policy "rls_test_select" on rls_testusing ( auth.uid() = user_id );
而应该使用:
123create 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-role | 170 | < 0.1 | 99.78% | 优化前: 无 TO 策略优化后: TO authenticated (匿名用户访问) |