行级安全
使用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密钥的区别
使用anonPostgres角色与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 (匿名用户访问) |