行级别安全
使用 Postgres 行级别安全性来保护你的数据。
当你需要细粒度的授权规则时,没有比 Postgres 的 行级别安全性 (RLS) 更好的选择了。
Supabase 中的行级别安全性#
只要你启用了 RLS,Supabase 就能从浏览器安全便捷地访问数据。
RLS 必须 在任何存储在公开 schema 中的表上启用。默认情况下,这是 public schema。
使用仪表盘中的表格编辑器创建的表默认启用 RLS。如果你在原始 SQL 或 SQL 编辑器中创建表,请记住自行启用 RLS
1alter table <schema_name>.<table_name>2enable row level security;RLS 功能强大且灵活,允许你编写符合你独特业务需求的复杂 SQL 规则。RLS 可以与 Supabase Auth 结合使用,从而从浏览器到数据库实现端到端的用户安全。
RLS 是 Postgres 的基本功能,即使通过第三方工具访问,也能提供 "纵深防御" 来保护你的数据,防止恶意攻击者。
策略#
策略 是 Postgres 的规则引擎。一旦你掌握了它们,策略就很容易理解。每个策略都附加到一个表,并且每次访问该表时都会执行该策略。
你可以将它们视为为每个查询添加一个 WHERE 子句。例如,像这样的策略...
1create policy "Individuals can view their own todos."2on todos for select3using ( (select auth.uid()) = user_id );.. 当用户尝试从 todos 表中选择数据时,将会转换为以下内容
1select *2from todos3where auth.uid() = todos.user_id;4-- Policy is implicitly added.启用行级别安全性#
你可以使用 enable row level security 子句为任何表启用 RLS
1alter table "table_name" enable row level security;一旦你启用了 RLS,在创建策略之前,将无法通过 API 使用公共 anon 密钥访问任何数据。
为新表自动启用 RLS#
如果你希望为新表自动启用 RLS,你可以创建一个事件触发器,在表创建后运行。这使用 Postgres 事件触发器 来调用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY,从而在新创建的每个表上启用 RLS。
1CREATE OR REPLACE FUNCTION rls_auto_enable()2RETURNS EVENT_TRIGGER3LANGUAGE plpgsql4SECURITY DEFINER5SET search_path = pg_catalog6AS $$7DECLARE8 cmd record;9BEGIN10 FOR cmd IN11 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 LOOP16 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%' THEN17 BEGIN18 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 EXCEPTION21 WHEN OTHERS THEN22 RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;23 END;24 ELSE25 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;28END;29$$;3031DROP EVENT TRIGGER IF EXISTS ensure_rls;32CREATE EVENT TRIGGER ensure_rls33ON ddl_command_end34WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')35EXECUTE FUNCTION rls_auto_enable();请注意,这适用于在安装触发器后创建的表。现有表仍然需要手动启用 RLS。
`auth.uid()` 在未认证时返回 `null`
当没有经过身份验证的用户发起请求时(例如,未提供访问令牌或会话已过期),auth.uid() 返回 null。
这意味着像这样的策略
1USING (auth.uid() = user_id)对于未经验证的用户将无声地失败,因为
1null = user_id在 SQL 中始终为 false。
为了避免混淆并明确你的意图,我们建议显式检查身份验证
1USING (auth.uid() IS NOT NULL AND auth.uid() = user_id)已认证和未认证的角色#
Supabase 将每个请求映射到以下角色之一
anon:未经验证的请求(用户未登录)authenticated:已经验证的请求(用户已登录)
这些实际上是 Postgres 角色。你可以使用 TO 子句在你的策略中使用这些角色
1create policy "Profiles are viewable by everyone"2on profiles for select3to authenticated, anon4using ( true );56-- OR78create policy "Public profiles are viewable only by authenticated users"9on profiles for select10to authenticated11using ( true );匿名用户与 anon 密钥
使用 anon Postgres 角色与 Supabase Auth 中的 匿名用户 不同。匿名用户假设 authenticated 角色来访问数据库,并且可以通过检查 JWT 中的 is_anonymous 声明来与永久用户区分开来。
创建策略#
策略是附加到 Postgres 表的 SQL 逻辑。你可以将任意数量的策略附加到每个表。
如果使用 Supabase Auth,Supabase 提供了一些 助手 来简化 RLS。我们将使用这些助手来说明一些基本策略
SELECT 策略#
你可以使用 using 子句指定 SELECT 策略。
假设你有一个名为 profiles 的表在 public schema 中,并且你希望启用对所有人的读取访问权限。
1-- 1. Create table2create table profiles (3 id uuid primary key,4 user_id uuid references auth.users,5 avatar_url text6);78-- 2. Enable RLS9alter table profiles enable row level security;1011-- 3. Create Policy12create policy "Public profiles are visible to everyone."13on profiles for select14to anon -- the Postgres Role (recommended)15using ( true ); -- the actual Policy或者,如果你只想让用户能够看到他们自己的个人资料
1create policy "User can see their own profile only."2on profiles3for select using ( (select auth.uid()) = user_id );INSERT 策略#
你可以使用 with check 子句指定 INSERT 策略。with check 表达式确保任何新的行数据都符合策略约束。
假设你有一个名为 profiles 的表在 public schema 中,并且你只想让用户能够为自己创建个人资料。在这种情况下,我们希望检查他们的用户 ID 与他们尝试插入的值是否匹配
1-- 1. Create table2create table profiles (3 id uuid primary key,4 user_id uuid references auth.users,5 avatar_url text6);78-- 2. Enable RLS9alter table profiles enable row level security;1011-- 3. Create Policy12create policy "Users can create a profile."13on profiles for insert14to authenticated -- the Postgres Role (recommended)15with check ( (select auth.uid()) = user_id ); -- the actual PolicyUPDATE 策略#
你可以通过组合 using 和 with check 表达式来指定 UPDATE 策略。
using 子句表示允许更新的条件,而 with check 子句确保所做的更新符合策略约束。
假设你有一个名为 profiles 的表在 public schema 中,并且你只想让用户能够更新他们自己的个人资料。
你可以创建一个策略,其中 using 子句检查用户是否拥有正在更新的个人资料。并且 with check 子句确保在结果行中,用户不会将 user_id 更改为不等于其用户 ID 的值,从而保持修改后的个人资料仍然满足所有权条件。
1-- 1. Create table2create table profiles (3 id uuid primary key,4 user_id uuid references auth.users,5 avatar_url text6);78-- 2. Enable RLS9alter table profiles enable row level security;1011-- 3. Create Policy12create policy "Users can update their own profile."13on profiles for update14to authenticated -- the Postgres Role (recommended)15using ( (select auth.uid()) = user_id ) -- checks if the existing row complies with the policy expression16with check ( (select auth.uid()) = user_id ); -- checks if the new row complies with the policy expression如果未定义 with check 表达式,则 using 表达式将同时用于确定哪些行可见(正常的 USING 情况)和哪些新行将被允许添加(WITH CHECK 情况)。
要执行 UPDATE 操作,需要相应的 SELECT 策略。如果没有 SELECT 策略,UPDATE 操作将无法按预期工作。
DELETE 策略#
你可以使用 using 子句指定 DELETE 策略。
假设你有一个名为 profiles 的表在 public schema 中,并且你只想让用户能够删除他们自己的个人资料
1-- 1. Create table2create table profiles (3 id uuid primary key,4 user_id uuid references auth.users,5 avatar_url text6);78-- 2. Enable RLS9alter table profiles enable row level security;1011-- 3. Create Policy12create policy "Users can delete a profile."13on profiles for delete14to authenticated -- the Postgres Role (recommended)15using ( (select auth.uid()) = user_id ); -- the actual Policy视图#
由于它们通常使用 postgres 用户创建,因此视图默认绕过 RLS。这是 Postgres 的一项功能,它会自动使用 security definer 创建视图。
在 Postgres 15 及更高版本中,你可以通过设置 security_invoker = true 来使视图遵守底层表的 RLS 策略,当由 anon 和 authenticated 角色调用时。
1create view <VIEW_NAME>2with(security_invoker = true)3as select <QUERY>在旧版本的 Postgres 中,通过从 anon 和 authenticated 角色撤销访问权限,或将它们放在未公开的 schema 中来保护你的视图。
助手函数#
Supabase 提供了一些助手函数,可以更轻松地编写策略。
auth.uid()#
返回发起请求的用户的 ID。
auth.jwt()#
不应将 JWT 中存在的所有信息都用于 RLS 策略。例如,创建一个依赖于 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 数组
1create policy "User is in team"2on my_table3to authenticated4using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));请记住,JWT 并不总是“新鲜的”。在上面的示例中,即使你从团队中删除用户并更新 app_metadata 字段,在用户的 JWT 刷新之前,也不会反映出来。
此外,如果你正在使用 Cookies 进行身份验证,则必须注意 JWT 的大小。某些浏览器将每个 Cookie 的限制设置为 4096 字节,因此你的 JWT 的总大小应足够小,才能适合此限制。
MFA#
可以使用 auth.jwt() 函数来检查 多因素身份验证。例如,你可以限制用户在没有至少 2 级身份验证(保证级别 2)的情况下更新他们的个人资料
1create policy "Restrict updates."2on profiles3as restrictive4for update5to authenticated using (6 (select auth.jwt()->>'aal') = 'aal2'7);绕过行级别安全性#
Supabase 提供特殊的“服务”密钥,可用于绕过 RLS。这些绝不能在浏览器中使用或暴露给客户,但它们对于管理任务很有用。
即使客户端库使用服务密钥初始化,Supabase 仍将遵守已登录用户的 RLS 策略。
你还可以创建新的 Postgres 角色,这些角色可以使用“绕过 RLS”权限来绕过行级别安全性
1alter role "role_name" with bypassrls;这对于系统级访问很有用。你绝不 应该与任何具有此权限的 Postgres 角色的登录凭据共享。
RLS 性能建议#
每个授权系统都会对性能产生影响。虽然行级别安全性功能强大,但性能影响很重要。这对于扫描表中每一行的查询(例如,许多 select 操作,包括那些使用 limit、offset 和 ordering 的操作)尤其如此。
基于一系列 测试,我们有一些关于 RLS 的建议
添加索引#
确保你已在策略中使用的任何未索引的列(或主键)上添加 索引。对于像这样的策略
1create policy "rls_test_select" on test_table2to authenticated3using ( (select auth.uid()) = user_id );你可以添加一个索引,例如
1create index userid2on test_table3using btree (user_id);基准测试#
| 测试 | 之前 (ms) | 之后 (ms) | % 提升 | 变更 |
|---|---|---|---|---|
| test1-indexed | 171 | < 0.1 | 99.94% | 之前 没有索引 之后 user_id 已索引 |
使用 select 调用函数#
您可以使用 select 语句来改进使用函数的策略。例如,不要使用以下方式
1create policy "rls_test_select" on test_table2to authenticated3using ( auth.uid() = user_id );您可以这样做
1create policy "rls_test_select" on test_table2to authenticated3using ( (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())) |
为每个查询添加筛选器#
策略是“隐式 where 子句”,因此通常会运行不带任何筛选器的 select 语句。这对性能来说是一个坏模式。不要这样做(JS 客户端示例)
1const { data } = supabase2 .from('table')3 .select()您应该始终添加一个筛选器
1const { data } = supabase2 .from('table')3 .select()4 .eq('user_id', userId)即使这会复制策略的内容,Postgres 也可以使用筛选器来构建更好的查询计划。
基准测试#
| 测试 | 之前 (ms) | 之后 (ms) | % 提升 | 变更 |
|---|---|---|---|---|
| test3-addfilter | 171 | 9 | 94.74% | 之前 auth.uid() = user_id之后 添加 .eq 或 where 到 user_id |
使用 security definer 函数#
“security definer” 函数使用创建该函数的相同角色运行。这意味着,如果您创建一个具有超级用户权限的角色(例如 postgres),那么该函数将具有 bypassrls 权限。例如,如果您有一个像这样的策略
1create policy "rls_test_select" on test_table2to authenticated3using (4 exists (5 select 1 from roles_table6 where (select auth.uid()) = user_id and role = 'good_role'7 )8);我们可以创建一个 security definer 函数,它可以扫描 roles_table 而没有任何 RLS 处罚
1create function private.has_good_role()2returns boolean3language plpgsql4security definer -- will run as the creator5as $$6begin7 return exists (8 select 1 from roles_table9 where (select auth.uid()) = user_id and role = 'good_role'10 );11end;12$$;1314-- Update our policy to use this function:15create policy "rls_test_select"16on test_table17to authenticated18using ( (select private.has_good_role()) );Security-definer 函数不应在您的 API 设置`中的“暴露模式”内创建一个模式。
最小化连接#
您通常可以重写您的策略以避免源表和目标表之间的连接。相反,尝试将您的策略组织为将目标表中的所有相关数据获取到一个数组或集合中,然后您可以在筛选器中使用 IN 或 ANY 操作。
例如,这是一个缓慢策略的示例,它将源 test_table 连接到目标 team_user
1create policy "rls_test_select" on test_table2to authenticated3using (4 (select auth.uid()) in (5 select user_id6 from team_user7 where team_user.team_id = team_id -- joins to the source "test_table.team_id"8 )9);我们可以重写它以避免此连接,而是将筛选条件选择到一个集合中
1create policy "rls_test_select" on test_table2to authenticated3using (4 team_id in (5 select team_id6 from team_user7 where user_id = (select auth.uid()) -- no join8 )9);在这种情况下,您还可以考虑 使用 security definer 函数 来绕过连接表上的 RLS。
如果列表超过 1000 个项目,可能需要采用不同的方法,或者您需要分析该方法以确保性能是可以接受的。
基准测试#
| 测试 | 之前 (ms) | 之后 (ms) | % 提升 | 变更 |
|---|---|---|---|---|
| test5-fixed-join | 9,000 | 20 | 99.78% | 之前 auth.uid() 在 col 上的表连接之后 col 在 auth.uid() 上的表连接 |
在您的策略中指定角色#
始终在您的策略中使用 Role,由 TO 运算符指定。例如,不要使用此查询
1create policy "rls_test_select" on rls_test2using ( auth.uid() = user_id );用途
1create policy "rls_test_select" on rls_test2to authenticated3using ( (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 (anon 访问) |
更多资源#
- 测试你的数据库
- RLS 指南和最佳实践
- 使用 pgTAP 和 dbdev 测试 RLS 的社区仓库