一个反例:某公司把 service_role 密钥提交到了前端仓库的配置文件里,结果被爬到,攻击者用它绕过 RLS 把整库用户表拖走。这起事故里,Supabase 的安全机制没问题,问题在密钥管理。这一节我们分层讲安全:网络、认证、授权、数据、合规,每层该守什么。
把安全想成建筑的纵深防御:外墙、门禁、保险柜、监控各管一层,任何一层漏了还有下一层。Supabase 的安全层次:
下面 SVG 把这个分层画成同心圆,外层破了内层仍挡:

两条铁律:
anon key 可公开(它只是标识项目,真正权限由 RLS 收口),但别把它和敏感配置混为一谈。service_role 永远只在受信任服务端,绝不进前端、不进客户端打包、不进 Git。用环境变量管理,而非硬编码:
// 正确:从环境变量读(服务端) const admin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // 仅函数/服务端可用 )
# SUPABASE_SERVICE_ROLE_KEY=eyJ...
前面多节讲过 RLS,这里强调两个常见漏洞:
enable row level security,且没策略,结果是匿名也能读全表。Supabase 默认新表是关 RLS 的,必须显式开。using (true) 又忘了区分读写:公开读的表若也用 for all using (true),等于任何人能改能删。读宽写窄要分开写。-- 正确拆分:读开放、写收紧 create policy "公开读" on public.posts for select using (true); create policy "作者写" on public.posts for all using (auth.uid() = author_id) with check (auth.uid() = author_id);
若业务要过等保、GDPR 之类,Supabase 提供的能力:
pg_dump 走人,满足"数据可携带"。-- 满足"被遗忘权":删用户级联清掉其全部业务数据 create table public.posts ( id bigint generated always as identity primary key, author_id uuid references auth.users(id) on delete cascade ); -- 删除 auth.users 时,相关 posts 自动随删
背景:某社交应用新加 messages 表,开发赶工只写了 for select using (true),想"先让大家都能看,后面再收"。
操作过程:
GET /rest/v1/messages 都能拉到全部私信。alter table public.messages enable row level security; create policy "参与者可读" on public.messages for select using ( auth.uid() = sender_id or auth.uid() = receiver_id ); create policy "本人可发" on public.messages for insert with check (auth.uid() = sender_id);
结果:漏洞在发现当天修复,未造成大规模泄漏。
解读:这个案例的教训是"先松后紧"在工程上几乎必翻车——上线压力下一忘,就成公开泄露。我们主张默认收紧,需要开放时显式加策略。
变式:若要做到"管理员可介入调解纠纷",再加一条 for select using (auth.jwt() ->> 'app_role' = 'admin'),但 admin 声明必须来自可信的登录流程,不是客户端自填。
提醒:新表默认 RLS 是关闭的。每建一张存敏感数据的表,第一件事就是 enable row level security 并至少一条策略,再写业务代码。
建议:我们建议把安全审计做成上线清单的一项:用匿名 token 跑一遍所有暴露的表接口,确认该空的空、该拒的拒。这一招能抓出绝大多数 RLS 漏配。
安全不是平均用力,资源该压在最容易出事的层。下面把各层常见失误和投入产出排个序,帮你把钱和精力花在刀刃上:
| 层 | 最常见失误 | 投入产出 | 我们的优先级 |
|---|---|---|---|
| 授权层(RLS) | 忘开 RLS / 先松后紧 | 极高 | 第一优先 |
| 运维层(密钥) | service_role 进前端 | 极高 | 第一优先 |
| 认证层 | 无 MFA、OAuth 回调校验漏 | 高 | 第二优先 |
| 网络层 | 无 IP 限制、数据库公网暴露 | 中 | 按需(合规驱动) |
| 数据层 | 无加密、无审计 | 中 | 云端默认已给,复查即可 |
💡 关键直觉:RLS 和密钥管理这两件事占安全事故的八成以上,且都是"写对一次、长期受益"的硬防线。把这两道焊死,剩下各层是锦上添花;反之这两道松了,网络层再厚也挡不住应用层泄数据。
背景:某账号系统只靠邮箱密码,攻击者用泄露的密码库批量撞库,一天登进十几个账号。
操作过程:
-- 查开启 MFA 的用户占比,确认覆盖度(需 auth 相关视图权限) select count(*) filter (where (raw_app_meta_data->'amr')::text like '%mfa%') as with_mfa, count(*) as total from auth.users; -- 输出示例: with_mfa=820, total=1000 → 82% 已覆盖
结果:MFA 把"密码泄露=账号失守"的等式打破,哪怕密码外流也不直接沦陷。
解读:MFA 是认证层性价比最高的加固。它不解决 RLS 问题,但把"拿到密码就能进"变成"拿到密码还得过第二道",把攻击成本抬到不划算。
⚠️ 常见坑:有人把 MFA 只当"登录时弹一下",却忘了"改密码、导出数据"等敏感操作也要二次验证。MFA 的价值在于保护敏感动作,不只保护登录那一刻——否则攻击者登录后直接改绑自己手机,原主反而被踢。
下一节讲部署与运维:怎么把本地结构推上多环境、怎么做 CI、怎么备份恢复。