深入解析 Supabase 四种密钥的用途与权限范围,以及 2025 年 11 月后新旧项目的密钥迁移截止时间节点。
我审计用 AI 工具构建的应用。在我发现的暴露凭证中,Supabase key 占大多数,但大多数情况下并不构成真正的问题。
这是两个方向上人们都容易搞错的地方。有人会因为一个本就设计为公开的 key 而惊慌失措。也有人把那个拥有完整数据库访问权限的 key 交付出去,却从未察觉,因为什么都"没坏"。
Supabase 有四个 key,其中两个即将被弃用。
Supabase 正在替换原有的 anon 和 service_role key。他们自己的时间表对截止时会发生什么写得很直白:
"旧版 API key 将被删除,并从文档和 Dashboard 中移除。你必须在此时之前迁移到使用新的 API key,否则你的应用将无法运行。"
这一定于 2026 年底,具体时间待确认。
但有一个更早的里程碑更值得关注:
"从 2025 年 11 月 1 日起恢复的项目将不再带有旧版 API key。新项目不再提供 anon 和 service_role 供使用。"
所以,如果你的项目创建于 2025 年 11 月之后,你已经有了新的 key,无需任何操作。如果创建于之前,你在使用旧版 key,且有一个截止期限。
想想这意味着什么。2025 年那一波用 Lovable、Bolt 和 Replit 构建的应用狂潮中,几乎所有应用都早于这个截止日期。那些应用运行着旧版 key,而它们的主人大多数从未打开过 API 设置页面。一旦 key 被删除,应用就停止运行了。
Source: Supabase, Understanding API keys.
这四个 key 中,有两个属于你的前端。如果另外两个到达了浏览器,意味着你的数据库已经沦陷。
公开 key 不是泄露
publishable key 本意就是要交付的。它放在你的 JavaScript bundle 里。任何打开开发者工具的人都能看到它——这就是设计。
这个 key 用于标识你的项目。它不是保护数据的东西。
Row level security(行级安全策略)才是保护数据的东西。key 让请求获得 anon 或 authenticated 角色,而你在每个表上写的策略决定这些角色能读写什么。
所以 key 的安全性完全取决于你的策略是否严密。如果每个表都有策略,公开它不会造成任何损失。但如果某个表关闭了 RLS,同样的 publishable key 能读取整张表。其他找到它的人也一样可以。
这是我审计中最常见的严重发现。不是凭证被盗,而是本来设计为公开的东西被放在了正确位置,但面对的是从未被给予规则的表。
Secret key:完整数据库访问权限,无视一切策略
secret key 完全绕过行级安全策略。它不遵守你的策略,因为它本就不该遵守。那是它存在的意义。它让你的服务器能够代表系统行事,而非代表登录用户。
如果它到达了浏览器,你写的所有保护瞬间荡然无存。不是被削弱,是完全消失。任何从 bundle 中读取它的人都可以读取、修改或删除数据库中的任意一行。
这个发现不存在"部分严重"的说法。把这个 key 视为已经泄露,因为网站的每个访客都已经能够读取它。
格式为何改变
以下是新版 key 解决的问题。
旧版 anon 和 service_role key 都是 JSON Web Token。两者都是同一串字符组成的长字符串。并排放在一起,你无法凭外观区分它们。放在每个访客浏览器里的 key 和拥有完整数据库访问权限的 key,唯一的区别在于 token 内部的一个字段。
我为 Framer 网站构建了一个安全扫描器——这是我经常遇到这个问题的地方:Framer 处理前端,所以任何有数据库在后面的东西都从浏览器端接入了 Supabase,而 key 就躺在页面里。扫描器解码 token 并根据 role claim 分支判断。service_role 报 critical。anon 完全不报告,因为报告它是错误的。一个把你的 publishable key 标出来的工具并没有发现什么,它只是让你知道它的输出是噪声,然后你下一次忽略的东西才是真正的问题。
新版格式消除了歧义。sb_publishable_ 和 sb_secret_ 一眼就能分辨,对人如此,对工具亦然。Supabase 还添加了浏览器检测,所以从浏览器使用 secret key 会返回 HTTP 401,而不是默默生效。
不要把这当作安全网。检查是基于 User-Agent header 进行的,Supabase 自己的文档说得明白:它"并不意味着攻击者不会用其他工具来使用它"。任何把你的 key 从页面里复制出来、用 curl 发送的人都能直接穿过。401 只是阻止你自己的代码意外运行。它阻止不了找到 key 的人。
这是一个真正的改进,值得把原因说清楚。旧设计让一个灾难性的错误和一个无害的设计看起来完全一样。
如何判断你交付的是哪一个
新版 key,看前缀。sb_publishable_ 可以放在前端。sb_secret_ 不可以。
旧版 key,前缀没用。必须解码 token 并读取 role 字段。
在本地做,不要用在线 JWT 解码器。如果这个 key 实际上是 service_role,你刚把完整数据库访问权限粘贴到了别人的网站上。在浏览器控制台里:
JSON.parse(atob(key.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))).role
它返回 anon 或 service_role。就这样。
split 和两个 replace 调用不是装饰。JWT 是三个 base64url 段用点号连接而成,所以直接对整个字符串调用 atob 会失败,而且 base64url 用 - 和 _,而 atob 期望 + 和 /。很多 token 恰好不包含这两个字符,所以那个简单的一行代码在大多数时候都能用——直到有一天它不能了。
然后检查每个 key 的使用位置。publishable 或 anon key 属于客户端代码。secret 或 service_role key 属于服务器,放在环境变量里,绝不进入仓库,绝不发送到浏览器。
如果错误的 key 已经公开了
这里新旧两套 key 系统不再等价,这是迁移最强有力的实际理由。
新版 key:替换它。在 Settings,API Keys 中创建第二个 secret key,把所有东西迁移过去,然后删除被泄露的那个。publishable key 不受影响,没有人会因此被登出,没有停机。删除 secret key 是永久性的,所以按这个顺序来。
旧版 key:做不到。Supabase 自己的故障排除文档说得很明确:"已经无法轮换旧版 anon、service 和 JWT secrets。"没有按钮。被暴露的 service_role key 会一直暴露着,直到你迁移出去。
这值得好好想一想。如果你现在用的是旧版 key,而且 service_role key 已经在浏览器 bundle 里了,迁移不是 2026 年截止日期前的一项家务活。撤销这个 key 的唯一办法就是迁移,而且这是这周就要做的事。
历史上修复方法是重新生成项目的 JWT secret,它同时为两个旧版 key 签名,所以会让所有会话一次性失效。Supabase 描述其效果为所有当前 secret "立即失效,使用它们的所有连接将被切断。"这条路现在已经关闭,替代方案是新 key——这就是整个迁移的核心:可单独撤销的凭证,而不是两个绑定在同一个 secret 上的 token。
然后检查你的行级安全策略。被暴露的 service_role key 通常意味着当时根本没人考虑过数据库访问规则,所以策略往往也是缺失的。
迁移出旧版 key
Supabase 的迁移指南覆盖了整个流程:
在 Settings,API Keys 中创建新 key
在客户端代码中将 anon key 替换为 publishable key
在服务器上将 service_role 替换为 secret key
更新 Edge Functions 以读取新的环境变量
确认没有任何地方仍在使用旧 key
停用旧版 key
两套 key 可以同时工作,所以你可以渐进迁移。而且停用旧版 key 是可逆的。如果你漏掉了一个客户端,重新打开它们就行。这比听起来更重要,因为担心破坏生产环境是这件事被一拖再拖的主要原因。
迁移中有五处容易出错的地方,都有记录:
两个新 key 都不放在 Authorization: Bearer header 里。只放在 apikey header 上发送。很多 Supabase 客户端默认把 key 放在两个 header 上,然后平台尝试把它解析为 JWT 并以 Invalid JWT 拒绝请求。这个错误信息让人在错误的地方找上几个小时。
Database Webhooks 和 pg_net 调用需要同样的更改。
不要把 secret key 硬编码在 SQL 或 webhook 配置里。用 Vault。
Edge Functions 无法在 Authorization header 中验证新 key,所以设置 verify_jwt = false。这使得函数可以被任何拥有 URL 的人公开调用,所以授权检查必须移到函数体内部。文档说"在你自己的代码中授权请求",这是人们容易跳过的一步,因为函数不管做不做这一步都能完美运行。
Public Realtime 连接限制为 24 小时,除非连接通过 Supabase Auth 或支持的第三方提供商升级为用户级认证。
打开你的 Supabase dashboard,进入 Settings,API Keys。如果你看到 anon 和 service_role,说明你的项目早于 2025 年 11 月,你有迁移要做。
顺带做三个检查:
确认每个表都启用了行级安全,然后检查策略。Enabled 不等于 protected。一个策略为 USING (true) 的表可以被持有你公钥的任何人读取,而那就是所有人,dashboard 仍然会显示 RLS 为 on。相同情况也适用于通过 SECURITY DEFINER 视图暴露的表,它以所有者身份运行并完全绕过策略。这两种都不显示为警告。
搜索你的前端代码和部署的 bundle 中的 service_role 和 sb_secret_。搜索构建产物,不只是源代码,因为环境变量在构建时会被内联——泄露通常就是这样发生的。如果你找到了,参见上面的部分:新版 key 可以替换,旧版 key 不行。
解码实际在你的客户端 bundle 中的 key,确认 role 显示为 anon。不是你的 .env 文件里的那个,是实际交付出去的那个。
这些只需要几分钟。它们不会告诉你你写的策略是否正确,或者后续更改是否重新打开了你已经关闭的东西。找到一个暴露的 key 和确认你的访问规则有效是两回事。在 AI 构建的应用中,第二个才是大多数工作所在。
如果你不想自己检查
第二个问题就是我大部分时间在做的事。我审计用 AI 工具构建的应用,逐账户逐个检查访问规则,然后发回一份报告,列出每个发现、它在哪里以及如何更改。固定价格,750 美元起。这就是我们做的审计,你可以在这里申请。
Understanding API keys, Supabase Docs
Migrating to publishable and secret API keys, Supabase Docs
Upcoming changes to Supabase API Keys, Discussion #29260
Rotating Anon, Service, and JWT Secrets, Supabase Docs
Originally published at thunkle.ai.