Supabase 行级安全:AI 生成的移动应用数据从第一天就该这么做
AI 生成的移动应用上线快,因为后端"感觉被解决了":加 Supabase auth、建几张表、把 anon key 粘进 Expo 配置、行就开始流动。这个速度掩盖了一个不舒服的事实:anon key 打进了客户端包,任何人都能从 bundle 里扒出来直接调你的数据库。
行级安全(Row Level Security, RLS)就是让这层安全的机制。它在 Postgres 内部运行、作用于每个查询,无论请求来自哪个客户端或脚本。配置良好时:登录用户只能看到自己的行,匿名访客只能看到真正公开的,其余的在离开数据库之前就被拒绝。缺失时:应用测试时完美、生产里悄悄漏数据——你不会收到报错,只是表会对任何提问者敞开心扉。
为什么移动应用不能跳过 RLS
Web 应用可以把 service key 藏服务器上,移动应用不行。Expo 构建包按设计包含可发布的 anon key,Supabase 把该 key 的每个请求映射到 anon 角色或 authenticated 角色。Grant 决定角色能否运行某个操作,policy 决定操作触及哪些行——两个都要对。有 policy 没 grant 会以权限错误失败关闭;有 grant 没 policy 会失败开放、把行交出去。大多数 AI 生成的 schema 落在第二种:表建得快、RLS 从未启用、默认 grant 大敞着。
值得养成的习惯:暴露 schema 里的每张表都启用 RLS、最小 grant、每个实际用到的操作至少一条 policy。查找表不例外,v1 原型不例外。
grant 和 policy 怎么配合
把访问控制想成两道串联的门:请求先撞 grant,再撞 policy。Grant 是粗粒度的,说角色可以对表执行 select 或 insert;policy 是精确的,给每个查询加隐藏 WHERE 子句,通常比较行属主列与 JWT 里的登录用户 id。
典型的个人数据模式:
alter table public.reports enable row level security;
revoke all on table public.reports from anon, authenticated;
grant select, insert, update, delete on table public.reports to authenticated;
create policy "Users manage their own reports"
on public.reports for all to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
using 子句过滤读、更新、删除;with check 校验写。用 for all 加两个子句让规则对称:用户不能插入自己永远读不回来的行,也不能猜 id 改别人的行。
把 service_role 完全留在客户端之外。它设计上绕过 RLS,只属于服务端代码(可信边缘逻辑)。移动端需要特权写就路由到持有后端逻辑的 edge function,而不是放宽客户端 policy。
用 auth.uid() 拥有自己的行
AI 构建应用最简单耐用的模式是基于属主的访问:每张用户表加 user_id 列,默认取当前请求用户,每条 policy 比较 auth.uid()。
alter table public.todos
alter column user_id set default (select auth.uid());
这消灭了一整类客户端 bug——应用忘记发属主 id 或发错 id。数据库从 JWT 填上它,policy 在返回路上强制执行。
测试反面情况,不只测快乐路径。用户 A 登录,直接读用户 B 的行 id、试着更新、试着删除——三者都应返回空或拒绝。测试套件只查"属主能读自己的数据",只测了一半 policy。
公开读 + 私有写需要两条 policy
很多应用需要混合访问:公开档案谁都能看,只有属主能编辑。别用一条宽松 policy 解决,按操作写窄 policy:select policy 显式命名 authenticated 和 anon 两个角色——忘了撤销的 anon grant 加一条宽松 select policy,就是"公开读变公开一切"的方式。anon 只对真正公开的表授予只读,其余什么都不给。表永远不该让未登录访客看见,就完全不给 anon grant——grant 缺失是安全控制,不是疏忽。
团队工作区需要成员检查
单一属主 policy 在加共享项目、组织、家庭账户后就不够用了。下一步是成员表 + 穿过它的 policy:workspace_members 表(workspace_id、user_id)+ 带 workspace_id 的数据表,policy 查成员关系而非直接属主。跨表复用就把成员检查放进 security definer 小函数并固定 search_path。注意:RLS 在查询触及的每一行上运行,慢 policy = 慢应用,要确保 workspace_id 和 user_id 有索引。查看文档和删除工作区是不同权限,用成员行上的 role 列区分。
视图和函数留在同一边界内
视图默认在提权模式运行时会绕过 RLS——给基表加了安全,又在上面暴露了一个便利视图,就白干了。把每个视图当成自己的表面:要么以调用者权限运行(底层 policy 仍生效),要么给视图本身同样严格的规则。数据库函数同理:以属主身份运行的函数能绕过你刚写的 policy,优先用调用者权限执行用户作用域读取,把提权函数留给带显式输入验证的窄服务端任务。实用审计很短:列出触及用户数据的每个视图和函数,确认没有公开路径返回基表 policy 会拒绝的行。
上线前测试每张表
RLS 没有测试就是希望,不是控制。每张表对 select/insert/update/delete 断言允许和拒绝,anon 和 authenticated 两种角色、至少两个不同用户。最低测试集:匿名用户读不了私有行也写不了任何东西;属主能增删改查自己的行;第二个登录用户读、改、删不了第一个用户的行;公开表允许匿名读但拒绝匿名写;团队表允许成员读、拒绝非成员读。把这些存成 SQL 测试文件放在迁移旁边,随正常数据库测试命令运行。未来 AI 编辑加列或放宽 grant 时,套件在进入生产前抓住回归。套件通过之前,你不知道 policy 是否真的在干活。
来源:Supabase Row Level Security keeps AI-built mobile data safe from day one - DEV Community