行级安全策略
行级安全策略(Row Level Security,简称 RLS)是 Supabase 安全模型的核心。它允许你在数据库层面精确控制用户可以访问哪些数据行,是保护数据安全的关键机制。
什么是 RLS?
传统应用中,权限控制通常在应用层实现。但这存在安全隐患:如果 API 被绕过,或者开发者遗漏了权限检查,数据就可能泄露。
RLS 在数据库层面实现权限控制,无论数据通过什么方式访问(REST API、SQL、实时订阅),都会受到保护。
工作原理
RLS 的工作流程:
- 用户发起请求,携带 JWT 令牌
- PostgREST 解析令牌,获取用户 ID 和角色
- PostgreSQL 执行查询时,自动应用 RLS 策略
- 只有符合策略的数据行会被返回
用户请求 → JWT 认证 → 角色切换 → RLS 策略过滤 → 返回数据
启用 RLS
在 Dashboard 中启用
创建表时,Supabase 默认会启用 RLS。你也可以手动启用:
- 打开 Table Editor
- 选择表
- 点击 "RLS Policies" 标签
- 点击 "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 中创建
- 进入表的 "RLS Policies" 标签
- 点击 "New Policy"
- 选择模板或自定义策略
- 填写策略名称和条件
使用 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 仍然可以绕过
常见问题
策略不生效
检查以下几点:
- RLS 是否已启用:
select relname, relrowsecurity
from pg_class
where relname = 'posts';
- 策略是否正确:
select * from pg_policies where tablename = 'posts';
- 用户是否已登录:
select auth.uid();
性能问题
复杂的 RLS 策略可能影响查询性能:
- 为策略条件中的字段创建索引:
create index idx_posts_author on posts(author_id);
- 避免在策略中使用子查询,改用 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 后,你可以继续学习: