浏览器扩展藏不住密钥,接入架构才是关键
扩展代码会分发到用户设备,构建时注入环境变量、编码混淆或使用浏览器存储,都无法可靠隐藏 API 密钥。文章围绕三种替代接入模式展开,并提醒后端代理也需要防止被陌生人滥用。
AI 深度解读
Chrome 扩展里的 API key 藏不住。构建时注入环境变量、拆字符串、放进 WebAssembly,都改变不了一个事实:代码和请求最终运行在用户控制的设备上。原文给出的选择是,把自己的长期密钥留在后端;开发工具可以让用户自带密钥;临时令牌则只用于供应商明确支持的接口。
对做 AI 扩展的前端和全栈开发者,更值得读的是后半段:代理接口如果只负责转发请求,密钥虽然没泄漏,账单仍可能被陌生人刷爆。
为什么打包进去的密钥都算公开信息
扩展安装后,上传的签名 ZIP 会被解压到浏览器配置目录。公开扩展的安装包可以下载,JavaScript 可以直接搜索。攻击者不需要拿到源码仓库。
.env 最容易造成误解。构建工具把 process.env.OPENAI_API_KEY 替换成字符串常量,留在开发机上的是文件,进入产物的是密钥值。
Base64、字符编码、字符串拆分只是增加阅读成本。原文还指出,Chrome Web Store 的代码可读性规则将 Base64 和字符编码列入混淆情形,对应拒绝标识为 Red Titanium。WebAssembly 或 native helper 也无法让请求中的凭据消失;原文举例,扩展 DevTools 的 Network 面板能看到明文 Authorization。chrome.storage 同样挡不住浏览器的使用者。
只要长期密钥随扩展交付,就应该按已经公开来设计。
原文引用的 CRX-ray 研究发表于 ACM ASIA CCS,时间为 2026 年 6 月:扫描 Chrome、Firefox、Edge 的 163,924 个扩展,在 4,145 个扩展中发现 3,677 个不同的泄漏 API key。其中,309 个扩展暴露了 300 个 AI 平台密钥,合计覆盖超过百万用户。
这些是作者援引的研究结果,原文没有提供自己的扫描实验。它要说明的风险很具体:别人消耗你的额度,你承担账单;如果密钥写在安装包里,处理事故还会受商店审核影响。
三种方案,取决于谁承担调用成本
临时令牌并不是所有 AI API 都能用的通行证。OpenAI 的 Realtime API 支持为浏览器 WebRTC 会话签发 ephemeral keys;Google 的 ephemeral tokens 仍标为 preview,只适用于 Gemini Live API。它们也能被提取,风险较低靠的是有效期短。
开发者工具可以采用 BYOK,也就是用户自带密钥。原文建议存入 chrome.storage.local,不要使用会同步到各个已登录浏览器的 storage.sync;请求只发给供应商,不经过开发者服务器,并在隐私政策中说明。这里改变的是密钥归属和费用承担者,客户端可提取这个事实并没有消失。
普通文本生成则使用后端代理。扩展请求你的 API,服务器识别用户、判断权限,再补上真正的供应商密钥。
Gemini 用户还要单独核查凭据。按原文转述的 Google 文档,自 2026 年 5 月 28 日起,新建 AI Studio 密钥都是绑定服务账号的 authorization keys,Gemini API 会拒绝未受限制的 standard keys。原文没有展开旧密钥各类限制组合的迁移细节。这类限制覆盖网站、IP 和移动应用,不覆盖扩展 ID,也不能替代代理。
代理要限制任务,不能只转发请求
POST /v1/summarize 表达的是受控业务:给一段文字生成摘要。POST /v1/anything 则容易变成通用调用入口。如果客户端能随意指定模型、system prompt 和输出规模,服务器即使藏好了密钥,也可能替别人支付任意任务的费用。
原文要求服务器固定模型、system prompt、输入上限和 max_tokens,同时完成登录验证、调用前计数、每日 token 计量和通用错误处理。登录后的会话令牌应支持撤销,数据库保存其哈希值,减少数据库泄漏直接暴露有效会话的风险。
下面的示意处理器展示了如何先限制用户请求,再调用模型;辅助函数需要自行对接数据库和供应商。
// POST /v1/summarize
export async function summarizeHandler(req, res) {
const userId = await getUserId(req.headers.authorization);
if (!userId) return res.status(401).json({ error: 'sign_in_required' });
const text = String(req.body?.text ?? '').slice(0, 8000);
if (!text.trim()) return res.status(400).json({ error: 'text_required' });
const day = new Date().toISOString().slice(0, 10);
const count = await bumpCounter(userId, day); // atomic upsert, unique on (userId, day)
if (count > 50) return res.status(429).json({ error: 'daily_limit' });
try {
const { summary, usage } = await callModel({ text, maxTokens: 400 }); // model fixed server-side
await addTokens(userId, day, usage);
res.json({ summary });
} catch {
res.status(502).json({ error: 'failed' });
}
}
关键是 bumpCounter 位于 callModel 之前,而且必须原子更新,并为 (userId, day) 设置唯一约束,否则并发请求可能绕过限额。示例将输入截到 8000、每日调用限制为 50 次,并把 maxTokens 设为 400;这些是示例配置,不是通用推荐值。addTokens 记录消耗,可用于按用户和套餐核算成本,但代码没有展示 token 配额的拦截逻辑。
扩展侧应由 service worker 发起调用,避免把凭据交给 content script。原文用「内容脚本位于网页中」概括风险,这个解释比较粗;实际落地仍需检查消息桥接和数据传递边界。
具体配置包括:在 host_permissions 添加 API origin,消息监听器检查 sender.id === chrome.runtime.id,会话令牌放在 chrome.storage.local。客户端会话令牌可以存在,因为它按用户划分、会过期、能撤销,并受配额限制。服务器还要使用 HTTPS、增加边缘层每 IP 限流,并配置供应商消费告警。
我的判断
作者最有价值的判断,是把「藏密钥」推进到「控制谁能花多少钱」。对于开发者承担费用的 AI 功能,固定任务接口比通用转发接口更容易约束成本,也更容易排查滥用。
但这篇不能直接当成生产方案。它没有提供不同方案的对照测试,也没有证明 50 次调用、8000 输入长度、400 输出 token 适合一般团队。原文提到另有约 120 行的完整 Node.js 服务端及配套代码,本篇展示的仍是骨架。
尤其要区分请求次数和实际费用:相同次数的请求,消耗可能差很多。示例在上游失败时也会占用次数,适合阻止循环重试,但是否返还额度需要产品规则。研究数字和 Gemini 政策在这里也只是原文引用,不能把它们写成站内已经独立复核的结论。
能拿来做什么
- 检查扩展构建产物中的供应商密钥,尤其是通过环境变量注入的值。发现泄漏后,在服务器部署替代密钥、停用旧密钥并审计账单;若旧密钥进入过安装包,还要发布更新,并假定已安装副本仍保存它。
- 给摘要等 AI 功能设计固定的 Node 接口,把模型、提示词和输出上限留在服务端,在调用供应商之前完成鉴权和原子计数。
- 对面向开发者的 BYOK 工具,核查存储位置和请求去向,确保使用
chrome.storage.local,并清楚说明密钥仅发送给供应商。 - 为每个用户记录每日 token 消耗,同时配置 IP 限流和消费告警。选用临时令牌前,先确认目标确实是 Realtime API 或 Gemini Live API 支持的场景。