我发布 GeminiOmni 后第一件崩溃的事

2026/04/29

我在一个周三下午发布了 GeminiOmni 的第一个公开版本。那是一个精简的落地页,只有一个可用的工具——早期的 Nano Banana 2 图片编辑器版本——以及一个写着“免费试用,无需注册”的按钮。

网站于柏林时间 14:42 上线。到 15:05,我的 Google Cloud 计费仪表盘开始尖叫。

这篇文章完整剖析了哪里出了问题、损失了多少,以及我在当天工作结束前就做了哪些改变。如果你在构建任何用到付费 AI API 的东西,把这当作一堂免费课——大多数人不会写那些令人尴尬的部分。

实际经过

以下是我的事件日志中的时间线(是的,我确实有一个——推荐你也记一个):

  • 14:42 部署成功。网站上线 geminiomni-ai.com。我在一个小型 Mastodon 实例上发布了快速演示,然后去冲了杯咖啡。
  • 14:58 回来看到 Slack 上来自 Google Cloud 的通知:“您当前周期的账单已超过 5 美元。”这很令人担忧,因为这个周期才开始了 16 分钟。
  • 15:05 登录 Gemini API 控制台。看到过去 20 分钟内有 412 次图片生成请求,来自三大洲的 38 个不同 IP。
  • 15:08 将网站下线。注销了 API 密钥。生成了一个新的但暂时不使用。
  • 15:42 开始重写架构。
  • 18:30 发布了带有修复的 v2 版本。网站重新上线。
  • 23:00 这四小时“冒险”的总账单:84.30 美元

为了提供背景,我为整个第一个月的 API 支出预算了 200 美元。我因为一个本该知道要避免的错误,在 17 分钟内就烧掉了其中的 42%。

我之前做了什么

图片编辑器的第一个版本是这样连接的:一个 Next.js 页面,其中的 React 组件接受用户的提示词和上传的图片,然后直接通过浏览器使用 @google/genai 客户端和一个嵌入在 NEXT_PUBLIC_GEMINI_API_KEY 中的 API 密钥调用 Google 的 Gemini Image API。

是的。我把一个付费 API 密钥放在了 NEXT_PUBLIC_* 里。Next.js 尽职尽责地将其打包到客户端 JavaScript 中。而任何打开 DevTools 的人都能看到它。

我的辩解是——这个辩解并不太好——原始原型只是一个私有的 localhost 实验。我把它作为客户端环境变量连接是因为我当时迭代很快,不想费心去设置 API 路由。当我准备公开部署时,我改了大约四十样东西,唯独没有改密钥的位置。

这个错误并非只有我犯。在 2024-2025 年,我认识的好几个人都做过类似的变体。这是我所见过的最常见的可预防 AI 成本事故。

为什么这么快就被抓取了

令我惊讶的细节是请求来得如此之快。我发布的 Mastodon 实例大概有 600 个关注者,其中没有人会恶意这么做。流量肯定来自其他地方。

以下是我根据请求日志推断出的情况:

  1. 在上线五分钟内,一个自动爬虫访问了网站,从 JavaScript 中提取了打包的 NEXT_PUBLIC_GEMINI_API_KEY,并将其发布到一个我之前不知道的密钥交易频道。
  2. 十分钟内,大约 38 个不同的 IP 开始使用该密钥生成与我产品无关的图片。我在日志中抽样看到的提示词大多是通用的(“一辆红色跑车”,“森林里的动漫女孩”)。有少数是在用对抗性提示词测试模型极限。
  3. 到了第十五分钟,有人正在以 Google 允许的最大速度运行一个紧密循环来生成图片。

这是我想让独立开发者明白的部分:存在着一些爬虫,它们唯一的工作就是抓取新网站上的 API 密钥。它们监控最近的 SSL 证书签发,攻击新域名,解析 JavaScript 代码包,并在几分钟内发布发现的密钥。你不需要出名,也不需要出现在热门 subreddit 上。你只要在线并泄露了密钥就行。

我当天下午推出的架构

修复是结构性的,而非表面性的。我重写了数据流,使得没有任何 API 密钥会发送到浏览器,这个架构就是我们今天使用的:

浏览器(永远不保存密钥)
   ↓ POST /api/ai/generate { prompt, imageFile }
Next.js API 路由(服务端)
   ↓ envConfigs.gemini_api_key(保存在服务器上)
Google Gemini API

三个具体改动:

1. 将 API 密钥移至仅服务器可用的环境变量。NEXT_PUBLIC_GEMINI_API_KEY 重命名为 GEMINI_API_KEY。Next.js 默认会拒绝将仅服务器环境变量发送到客户端。双重保险:我添加了一个启动检查,如果任何包含“API_KEY”或“SECRET”的环境变量名以 NEXT_PUBLIC_ 开头,就会崩溃应用程序。

2. 创建一个 /api/ai/* 代理。 所有模型调用都通过一个服务端端点。浏览器发送提示词和任何用户上传的文件;服务器附加 API 密钥,发起实际的 Gemini API 调用,并仅返回结果。这听起来很明显,但这就是整个修复方案。

3. 在代理上添加基于 IP 的速率限制。 即使密钥安全了,我也不希望一个用户在免费层刷屏生成来耗尽配额。代理在免费层限制每个 IP 每分钟 10 次请求,在 Pro+ 层限制每分钟 60 次。这本来无法防止最初的事件——攻击者在轮换 IP——但它会让下一类滥用行为变得困难得多。

总的代码变更大约是新增 180 行,删除 90 行。从“网站下线”到“网站以新架构重新上线”,我花了 2 小时 48 分钟。不算很好,但也不算灾难。

下次我会怎么做

诚实的答案是:永远不要发布一个对付费服务进行客户端 API 调用的公开 AI 应用。 不能是“只是一个原型”,不能是“MVP 测试”,也不能是“给朋友的快速演示”。威胁模型不是恶意用户,而是已经存在、专门针对这种场景的自动化基础设施。

从这次事件中还得出了一些其他教训:

部署前检查代码包。 在发布之前,运行 grep -r "NEXT_PUBLIC_" .next/static/ 并检查里面的内容。如果有任何看起来像凭证的东西,在上线前修复它。我现在将其作为一个部署前的 git 钩子。

设置硬性的每日上限。 Google Cloud 允许你以每日美元金额为 Gemini API 设置上限。我将第一个月设置为 50 美元,之后设置为 200 美元。如果达到上限,API 会返回错误,我会收到一封邮件——这比让它在一个糟糕的夜里跑到 5000 美元要好得多。

使用成本最低的可发货模型。 我在免费层界面上烧钱做 Nano Banana 2 4K 编辑,每次成本 0.151 美元。即使在攻击发生之前,我的单位经济性也很脆弱。现在我通过 Nano Banana 2K(每次编辑 0.045 美元)路由免费层的生成,而将 4K 留给 Pro+。3.4 倍的成本降低会让 84 美元的账单接近 25 美元。

限制提示词长度。 其中一个滥用者发送了包含嵌入图片数据的大量提示词,而 Gemini 是按 token 计费的。代理现在将传入提示词的大小硬性上限设为 50KB。合法用户从未触及这个限制;而滥用者在第一次请求时就会命中。

收据

对于任何想验证我不是在编造的人:我 4 月 28 日的 Gemini API 控制台显示,从 14:42 到 15:08 有 411 次请求,费用报告显示图片生成收费 83.96 美元,输入 token 收费 0.34 美元。我当晚给 Google Cloud 支持发了邮件,他们拒绝退款——这很合理,密钥确实是我的,而且我授权了它的使用。他们建议的架构正是我已经部署的那个。

关键结论,写得足够大,我用手机在房间另一头也能看清:

AI API 密钥是承载凭证。它们应该在服务器上,永远不该去其他地方。

如果你只能从这篇文章中带走一件事,就带走这个。

继续阅读

—— Lena

Lena Hoffmann

Lena Hoffmann