跳到主要内容

行级安全策略

行级安全策略(Row Level Security,简称 RLS)是 Supabase 安全模型的核心。它允许你在数据库层面精确控制用户可以访问哪些数据行,是保护数据安全的关键机制。

什么是 RLS?

传统应用中,权限控制通常在应用层实现。但这存在安全隐患:如果 API 被绕过,或者开发者遗漏了权限检查,数据就可能泄露。

RLS 在数据库层面实现权限控制,无论数据通过什么方式访问(REST API、SQL、实时订阅),都会受到保护。

工作原理

RLS 的工作流程:

  1. 用户发起请求,携带 JWT 令牌
  2. PostgREST 解析令牌,获取用户 ID 和角色
  3. PostgreSQL 执行查询时,自动应用 RLS 策略
  4. 只有符合策略的数据行会被返回
用户请求 → JWT 认证 → 角色切换 → RLS 策略过滤 → 返回数据

启用 RLS

在 Dashboard 中启用

创建表时,Supabase 默认会启用 RLS。你也可以手动启用:

  1. 打开 Table Editor
  2. 选择表
  3. 点击 "RLS Policies" 标签
  4. 点击 "Enable RLS"

使用 SQL 启用

-- 启用 RLS
alter table posts enable row level security;

-- 禁用 RLS(不推荐)
alter table posts disable row level security;

启用 RLS 后,默认情况下所有用户都无法访问数据,需要创建策略来授权。

创建策略

策略定义了用户可以访问哪些数据。每个策略包含:

  • 命令类型:SELECT、INSERT、UPDATE、DELETE 或 ALL
  • 角色:策略适用的角色(anon、authenticated 或自定义角色)
  • 条件:使用 USING 子句定义访问条件

在 Dashboard 中创建

  1. 进入表的 "RLS Policies" 标签
  2. 点击 "New Policy"
  3. 选择模板或自定义策略
  4. 填写策略名称和条件

使用 SQL 创建

-- 基本语法
create policy "策略名称"
on 表名
for 命令类型 -- 可选,默认 ALL
to 角色 -- 可选,默认 public
using (条件) -- 查询条件
with check (条件); -- 插入/更新条件

常见策略模式

公开只读

允许所有人读取数据:

create policy "公开只读"
on posts
for select
to anon, authenticated
using (status = 'published');

这允许匿名用户和登录用户查看已发布的文章。

用户只能访问自己的数据

create policy "用户访问自己的数据"
on posts
for all
to authenticated
using (auth.uid() = author_id)
with check (auth.uid() = author_id);
  • using:控制查询时返回哪些行
  • with check:控制插入和更新时允许哪些行

auth.uid() 函数返回当前登录用户的 ID。

用户可以读取公开数据和自己数据

create policy "读取策略"
on posts
for select
to authenticated
using (
status = 'published'
or auth.uid() = author_id
);

管理员完全访问

创建管理员角色:

-- 创建管理员角色
create role admin;

-- 授予管理员权限
grant all on posts to admin;

-- 管理员策略
create policy "管理员完全访问"
on posts
for all
to admin
using (true)
with check (true);

插入时自动设置用户 ID

create policy "用户插入自己的数据"
on posts
for insert
to authenticated
with check (auth.uid() = author_id);

这样用户在插入数据时,必须将 author_id 设置为自己的 ID。

只能更新自己的数据

create policy "更新自己的数据"
on posts
for update
to authenticated
using (auth.uid() = author_id)
with check (auth.uid() = author_id);

只能删除自己的数据

create policy "删除自己的数据"
on posts
for delete
to authenticated
using (auth.uid() = author_id);

高级策略

基于角色字段

如果用户表有角色字段:

-- 用户表
create table profiles (
id uuid primary key references auth.users(id),
role text default 'user' check (role in ('user', 'moderator', 'admin'))
);

-- 版主可以管理所有文章
create policy "版主管理文章"
on posts
for all
to authenticated
using (
exists (
select 1 from profiles
where id = auth.uid() and role in ('moderator', 'admin')
)
);

基于组织成员

多租户场景:

-- 组织表
create table organizations (
id uuid primary key default gen_random_uuid(),
name text
);

-- 组织成员表
create table organization_members (
org_id uuid references organizations(id),
user_id uuid references auth.users(id),
role text default 'member',
primary key (org_id, user_id)
);

-- 项目表
create table projects (
id uuid primary key default gen_random_uuid(),
org_id uuid references organizations(id),
name text
);

-- 成员可以访问组织的项目
create policy "组织成员访问项目"
on projects
for select
to authenticated
using (
exists (
select 1 from organization_members
where org_id = projects.org_id
and user_id = auth.uid()
)
);

时间限制

只在特定时间段允许访问:

create policy "工作时间访问"
on sensitive_data
for select
to authenticated
using (
extract(hour from now()) between 9 and 18
);

IP 地址限制

结合自定义函数检查 IP:

create or replace function is_allowed_ip()
returns boolean as $$
declare
client_ip text;
begin
client_ip := current_setting('request.jwt.claims', true)::json->>'ip';
return client_ip like '192.168.%';
end;
$$ language plpgsql security definer;

create policy "内网访问"
on sensitive_data
for all
using (is_allowed_ip());

策略组合

多个策略之间是 OR 关系,只要满足任一策略即可访问:

-- 策略1:用户可以读取已发布文章
create policy "读取已发布"
on posts
for select
to authenticated
using (status = 'published');

-- 策略2:用户可以读取自己的文章
create policy "读取自己的"
on posts
for select
to authenticated
using (auth.uid() = author_id);

-- 结果:用户可以读取已发布文章 OR 自己的文章

查看和调试策略

查看表的所有策略

select 
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
from pg_policies
where tablename = 'posts';

测试策略

使用 set role 模拟用户:

-- 模拟特定用户
set request.jwt.claims = '{"sub": "user-uuid-here"}';

-- 执行查询
select * from posts;

-- 重置
reset request.jwt.claims;

使用 explain 分析

explain (costs off) select * from posts;

查看执行计划,确认 RLS 策略被正确应用。

绕过 RLS

某些场景需要绕过 RLS,例如后台管理任务。

使用 service_role 密钥

服务端使用 service_role 密钥可以绕过 RLS:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
'https://your-project.supabase.co',
process.env.SUPABASE_SERVICE_ROLE_KEY // 服务密钥
)

// 此查询不受 RLS 限制
const { data } = await supabase.from('posts').select('*')

创建绕过策略的角色

-- 创建服务角色
create role service_role noinherit;

-- 授予绕过 RLS 的权限
grant all on posts to service_role;
alter table posts force row level security; -- 强制 RLS
-- 注意:service_role 仍然可以绕过

常见问题

策略不生效

检查以下几点:

  1. RLS 是否已启用:
select relname, relrowsecurity 
from pg_class
where relname = 'posts';
  1. 策略是否正确:
select * from pg_policies where tablename = 'posts';
  1. 用户是否已登录:
select auth.uid();

性能问题

复杂的 RLS 策略可能影响查询性能:

  1. 为策略条件中的字段创建索引:
create index idx_posts_author on posts(author_id);
  1. 避免在策略中使用子查询,改用 JOIN:
-- 不推荐
using (exists (select 1 from profiles where id = auth.uid()))

-- 推荐:使用缓存或预计算

策略冲突

多个策略是 OR 关系,可能导致意外访问:

-- 策略1:所有人可以读取
create policy "公开读取" on posts for select using (true);

-- 策略2:只有作者可以更新
create policy "作者更新" on posts for update using (auth.uid() = author_id);

-- 问题:策略1 允许所有人读取,包括其他用户的数据
-- 解决:删除策略1 或修改条件

最佳实践

1. 默认拒绝

启用 RLS 后,默认拒绝所有访问。从最严格的策略开始,逐步放宽。

2. 分离读写策略

为不同的操作创建不同的策略:

-- 读取策略
create policy "读取策略" on posts for select ...;

-- 插入策略
create policy "插入策略" on posts for insert ...;

-- 更新策略
create policy "更新策略" on posts for update ...;

-- 删除策略
create policy "删除策略" on posts for delete ...;

3. 使用视图简化

创建视图封装复杂查询:

create view my_posts as
select * from posts where author_id = auth.uid();

-- 对视图设置策略
create policy "只能看到自己的文章"
on my_posts
for select
using (true);

4. 审计日志

创建触发器记录数据访问:

create table audit_log (
id bigint primary key generated always as identity,
table_name text,
operation text,
user_id uuid,
record_id bigint,
created_at timestamptz default now()
);

create function log_access()
returns trigger as $$
begin
insert into audit_log (table_name, operation, user_id, record_id)
values (tg_table_name, tg_op, auth.uid(), old.id);
return new;
end;
$$ language plpgsql security definer;

create trigger posts_audit
after update or delete on posts
for each row execute function log_access();

5. 测试覆盖

为 RLS 策略编写测试:

-- 测试用户只能看到自己的数据
do $$
declare
user_id uuid := gen_random_uuid();
post_count int;
begin
-- 模拟用户
perform set_config('request.jwt.claims', json_build_object('sub', user_id)::text, true);

-- 检查数据
select count(*) into post_count from posts where author_id != user_id;

assert post_count = 0, '用户看到了其他人的数据';
end $$;

下一步

掌握 RLS 后,你可以继续学习: