Supabase 行级安全策略 RLS
这是全教程最重要的一篇。RLS(Row Level Security,行级安全策略)决定谁能对哪些行做什么操作。前面学的所有内容,不配上 RLS 都是裸奔。很多新手跳过这一篇,结果产品上线后被用户扒数据,这是 Supabase 最不能省的知识点。
没有 RLS 的后果
回想第五篇,anon key 是公开的,任何人都能拿到。如果你的表没有 RLS,任何人打开浏览器控制台就能执行
// 查看所有用户的资料
await supabase.from('profiles').select('*')
// 删除所有数据
await supabase.from('profiles').delete().neq('id', '00000000-0000-0000-0000-000000000000')
这不是理论风险,只要你的网站被任何人访问过,就有人在裸奔的数据库上试过。记住一句话,没有 RLS 的表就是公开数据库。
RLS 是什么
RLS 是 PostgreSQL 自带的安全机制,Supabase 让它变成了产品的主打功能。可以把它想象成办公大楼的保安,每个人刷卡只能进自己的楼层,电梯会拦截没权限的人。
| 角色 | 电梯比喻 | 数据库对应 |
|---|---|---|
| 保安 | 控制谁能进哪层 | RLS 策略 |
| 员工卡 | 身份凭证 | 用户的 access_token |
| 楼层 | 允许访问的数据行 | 策略里筛选出的行 |
策略的本质是给每种操作定义一个条件,满足条件的行才放行。
开启 RLS
开启 RLS 本身很简单,一句 SQL
alter table public.profiles enable row level security;
开启后所有操作立刻被拦截,即使是你自己写的表,查询也会返回空数组。这不是坏了,是保安刚上岗,还没配任何通行规则。
给 profiles 表开完 RLS 后再执行第四篇的查询,结果一定是空。体会到这种”突然什么都查不到”的感觉,就理解了 RLS 的威力。
auth.uid() 是谁
写策略前先认识一个函数,auth.uid() 返回当前请求的用户 ID。未登录的请求返回 null,登录后返回 access_token 里解析出的用户 ID。
auth.uid() 就是保安读卡器,它从请求头里读身份,完全由 Supabase 托管,你不需要自己存任何会话状态。
策略四要素
一条完整的策略由四个部分组成
| 要素 | 说明 |
|---|---|
| 策略名 | 描述这条规则是干什么的 |
| 操作 | for select / insert / update / delete |
| 角色 | 默认 public,即所有角色,一般保持默认 |
| 条件 | using 和 with check 两套 |
using 决定哪些行可见,with check 决定写入的行是否合法。select 和 delete 只关心 using,insert 只关心 with check,update 两个都关心。update 的 using 管”能改哪些行”,with check 管”改完的结果合不合法”。
profiles 表完整策略
为 profiles 表写三组策略,覆盖一个用户对自己资料的完整操作
-- 用户只能读取自己的资料
create policy "用户读取自己的资料"
on public.profiles for select
using (auth.uid() = id);
-- 用户只能插入自己的资料
create policy "用户插入自己的资料"
on public.profiles for insert
with check (auth.uid() = id);
-- 用户只能更新自己的资料,且不能把 id 改成别人
create policy "用户更新自己的资料"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
执行完这三条,回前端用登录用户的会话查 profiles,只能看到自己那一行,改别人报错,改自己成功。
id 列与用户 ID 对应的关系是整套策略的关键。用自己资料表的 id 等于 auth.uid() 来表达”我的数据”。
允许所有人读的公开数据
不是所有表都私有。比如文章列表,人人都能看,只要读的权限放开,写仍然受控
-- 任何人可读已发布文章
create policy "公开读文章"
on public.posts for select
using (true);
-- 只有作者能写
create policy "作者管理自己的文章"
on public.posts for all
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
using (true) 表示不设条件,所有行可见。for all 是 select、insert、update、delete 四合一写法。
管理员的特殊通道
RLS 拦截所有匿名请求,但 service_role key 是特例。用 service_role key 发起的请求携带的角色是 service_role,默认绕过所有策略。
这解释了第二篇为什么强调 service_role key 不能出现在前端。它就像万能钥匙,只能放在自己的服务器上。
常见误区
| 误区 | 正解 |
|---|---|
| 登录了就能查所有数据 | 错,RLS 是独立的一层,登录只解决你是谁 |
| 在 Dashboard 里能查,前端查不到 | 正常,Dashboard 走的是特权通道 |
| 策略名随便起 | 建议起中文名,方便团队理解每条策略的意图 |
| RLS 只影响前端 | 错,任何通过 PostgreSQL 连接发起的请求都受策略约束 |
检查清单
上线前对照自查
- 每张业务表都执行了 enable row level security
- 每张表都有明确的 select、insert、update、delete 策略
- 需要公开的数据明确写了 using (true)
- service_role key 只存在服务端
RLS 是 Supabase 安全体系的灵魂,这篇内容值得反复读。下一篇学习多表关联,把数据结构设计得更合理。
下一篇 Supabase 数据关联查询