4.2 安全性与合规性


4.2 安全性与合规性

一个反例:某公司把 service_role 密钥提交到了前端仓库的配置文件里,结果被爬到,攻击者用它绕过 RLS 把整库用户表拖走。这起事故里,Supabase 的安全机制没问题,问题在密钥管理。这一节我们分层讲安全:网络、认证、授权、数据、合规,每层该守什么。

先破除一个误解:安全不是加一层就行

把安全想成建筑的纵深防御:外墙、门禁、保险柜、监控各管一层,任何一层漏了还有下一层。Supabase 的安全层次:

  • 网络层:VPC、IP 允许列表、私有端点。
  • 认证层:JWT 签名、MFA、第三方登录的 OAuth 安全。
  • 授权层:RLS 强制行级权限。
  • 数据层:加密静止、审计日志。
  • 运维层:密钥管理、最小权限。

下面 SVG 把这个分层画成同心圆,外层破了内层仍挡:

04-02-fig01

密钥管理:最该守的底线

两条铁律:

  1. anon key 可公开(它只是标识项目,真正权限由 RLS 收口),但别把它和敏感配置混为一谈。
  2. service_role 永远只在受信任服务端,绝不进前端、不进客户端打包、不进 Git。

用环境变量管理,而非硬编码:

// 正确:从环境变量读(服务端) const admin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // 仅函数/服务端可用 )
# SUPABASE_SERVICE_ROLE_KEY=eyJ...

RLS 是授权的主心骨

前面多节讲过 RLS,这里强调两个常见漏洞:

  • 忘记开 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 自动随删

展开案例:一次 RLS 误配导致的泄漏与修复

背景:某社交应用新加 messages 表,开发赶工只写了 for select using (true),想"先让大家都能看,后面再收"。

操作过程:

  1. 上线后,任何人(含未登录)调用 GET /rest/v1/messages 都能拉到全部私信。
  2. 安全审计用匿名 token 跑了一遍接口,发现返回了其他用户的私信,立刻告警。
  3. 修复:拆成读宽写窄 + 仅参与者可见:
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);
  1. 回归:用匿名 token 再拉,返回空;用发送方 token 只看到自己参与的会话。

结果:漏洞在发现当天修复,未造成大规模泄漏。

解读:这个案例的教训是"先松后紧"在工程上几乎必翻车——上线压力下一忘,就成公开泄露。我们主张默认收紧,需要开放时显式加策略。

变式:若要做到"管理员可介入调解纠纷",再加一条 for select using (auth.jwt() ->> 'app_role' = 'admin'),但 admin 声明必须来自可信的登录流程,不是客户端自填。

把这一节收个尾

  • 安全分层,RLS 是授权主心骨,但不是唯一层。
  • service_role 严禁进前端;anon 可公开但权限靠 RLS。
  • 默认收紧策略,别"先松后紧";合规靠加密、审计、可导出、级联删除支撑。

提醒:新表默认 RLS 是关闭的。每建一张存敏感数据的表,第一件事就是 enable row level security 并至少一条策略,再写业务代码。

建议:我们建议把安全审计做成上线清单的一项:用匿名 token 跑一遍所有暴露的表接口,确认该空的空、该拒的拒。这一招能抓出绝大多数 RLS 漏配。

多层防御的取舍对照

安全不是平均用力,资源该压在最容易出事的层。下面把各层常见失误和投入产出排个序,帮你把钱和精力花在刀刃上:

最常见失误 投入产出 我们的优先级
授权层(RLS) 忘开 RLS / 先松后紧 极高 第一优先
运维层(密钥) service_role 进前端 极高 第一优先
认证层 无 MFA、OAuth 回调校验漏 第二优先
网络层 无 IP 限制、数据库公网暴露 按需(合规驱动)
数据层 无加密、无审计 云端默认已给,复查即可

💡 关键直觉:RLS 和密钥管理这两件事占安全事故的八成以上,且都是"写对一次、长期受益"的硬防线。把这两道焊死,剩下各层是锦上添花;反之这两道松了,网络层再厚也挡不住应用层泄数据。

再展开一个案例:MFA 挡下的一次撞库

背景:某账号系统只靠邮箱密码,攻击者用泄露的密码库批量撞库,一天登进十几个账号。

操作过程:

  1. 在 Auth 设置开启 TOTP 多因素(MFA),要求敏感操作前二次验证。
  2. 开启后,撞库拿到的密码仍过不了第二因子,登录失败率立刻上升:
-- 查开启 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% 已覆盖
  1. 对未开启的用户,登录后强制引导绑定 TOTP。
  2. 撞库成功数从日均十余个降到 0。

结果:MFA 把"密码泄露=账号失守"的等式打破,哪怕密码外流也不直接沦陷。

解读:MFA 是认证层性价比最高的加固。它不解决 RLS 问题,但把"拿到密码就能进"变成"拿到密码还得过第二道",把攻击成本抬到不划算。

⚠️ 常见坑:有人把 MFA 只当"登录时弹一下",却忘了"改密码、导出数据"等敏感操作也要二次验证。MFA 的价值在于保护敏感动作,不只保护登录那一刻——否则攻击者登录后直接改绑自己手机,原主反而被踢。

下一节讲部署与运维:怎么把本地结构推上多环境、怎么做 CI、怎么备份恢复。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U