如果 Web 应用直接从浏览器调用 OpenAI、Claude 或 Gemini,你的 API 密钥就暴露在浏览器里,任何打开 DevTools 的人都能复制。2 分钟内可确认:打开 Network 标签页,发送一次 AI 请求,查找发往提供商的 Authorization、x-api-key 或 x-goog-api-key 请求头。如果看到自己的密钥,今天就轮换,并把调用移到服务端路由后面。
下文介绍检查、修复顺序,以及编码 Agent 如何测试修复后的路由。
为什么浏览器中暴露密钥是问题
浏览器中暴露的 API 密钥是随前端代码或网络流量发送的秘密,每个访问者都能读。如果浏览器发送密钥,用户就能复制并用你的账单调用提供商。
Google 的 Gemini API 密钥指南说“不要在 Web 或移动应用中硬编码 API 密钥”,因为“编译进客户端代码的密钥可被用户提取”,并建议“运行后端代理服务器发起实际 API 调用”。OpenAI 的 API 密钥安全指南规则相同:通过自己的后端路由请求,不要把密钥部署到客户端。
openai-node README 表示浏览器支持默认关闭,“以避免暴露秘密 API 凭据”,必须设置 dangerouslyAllowBrowser: true。Anthropic TypeScript SDK 使用相同选项名,并且只有请求携带 anthropic-dangerous-direct-browser-access: true 请求头时才接受浏览器 CORS 调用,见 Simon Willison 的记录。
这种模式在快速搭建的应用中很常见:最短路径是在 React 组件 fetch,密钥放在 VITE_ 或 NEXT_PUBLIC_ 变量。被盗密钥会被转卖用于批量推理,见被盗 AI API 密钥与 LLMjacking,你会在账单上发现。
如何检查(2 分钟)

请针对生产环境检查,不要用 localhost。
- 查看 Network(45 秒)。 打开网站,按 F12(Mac 为 Cmd+Option+I),选 Network,触发一个 AI 功能,例如发送聊天消息。按
openai、anthropic或googleapis过滤。如果请求直接发往api.openai.com、api.anthropic.com或generativelanguage.googleapis.com,点击并打开 Headers。Authorization: Bearer、x-api-key、x-goog-api-key或?key=中出现密钥就是暴露。 - 搜索 bundle(45 秒)。 按 Cmd+Option+F(Windows 为 Ctrl+Shift+F)搜索所有已加载脚本,搜索
sk-、sk-ant-、AIza和dangerouslyAllowBrowser。提供给浏览器的.js文件中任何命中都是泄露,即使请求尚未发出。 - 检查环境变量前缀(30 秒)。 打开
.env和托管控制台。构建工具会按设计复制部分变量:Vite 的VITE_*(见 Vite 文档)、Next.js 的NEXT_PUBLIC_*、Create React App 的REACT_APP_*、Expo 的EXPO_PUBLIC_*。这些前缀后的提供商密钥就是公开的。
终端扫描构建输出:
# 生产构建后,从项目根目录运行
grep -rnoE "sk-(proj-|ant-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}" dist/ build/ .next/static/ 2>/dev/null
grep -rn "dangerouslyAllowBrowser" dist/ build/ .next/static/ 2>/dev/null
输出为空才通过。Google 表示 2026 年 5 月 28 日后创建的新 AI Studio 密钥是“auth keys”,格式可能不同,所以还要搜索 generativelanguage.googleapis.com。只有浏览器只与自己的域名通信(如 /api/chat),且任何地方不出现提供商主机名或密钥时,才算通过。
密钥暴露后的修复顺序
| 步骤 | 做什么 | 为什么 | 时间 |
|---|---|---|---|
| 1. 轮换 | 创建新密钥,吊销泄露的旧密钥 | 旧密钥可能已被复制 | 5 分钟 |
| 2. 移到服务端 | 把调用放进持有密钥的服务端路由或边缘函数 | 防止新密钥同样泄露 | 30–60 分钟 |
| 3. 加认证和限流 | 要求会话,限制每用户请求数 | 否则代理成为免费 AI 端点 | 30 分钟 |
| 4. 设置支出上限 | 在各提供商账单设置预算或硬上限 | 限制最坏账单 | 10 分钟 |
| 5. CI 扫描 | 输出 bundle 出现密钥模式就让构建失败 | 防止下一个功能改回去 | 15 分钟 |
先部署新密钥到服务端,再吊销旧密钥,否则功能会中断。零停机轮换见 API 密钥轮换最佳实践。OpenAI 建议设置强制执行的支出上限,因为告警本身不能停止流量;Google Cloud 预算会提醒,但不会停止使用。
把调用移到服务端路由

后端代理路由接收用户提示词,在服务端加密钥,调用提供商,只返回答案。浏览器永远看不到密钥。下面是 Next.js OpenAI 路由;Claude 和 Gemini 结构相同:
// app/api/chat/route.ts(仅在服务端运行)
import OpenAI from "openai";
import { getSession } from "@/lib/auth"; // 你的认证辅助函数
import { checkRateLimit } from "@/lib/limits"; // 你的按用户限流器
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 没有 NEXT_PUBLIC_ 前缀
export async function POST(req: Request) {
const session = await getSession(req);
if (!session) return new Response("Unauthorized", { status: 401 });
if (!(await checkRateLimit(session.userId, 20))) { // 每分钟 20 次
return new Response("Too many requests", { status: 429 });
}
const { message } = await req.json();
if (typeof message !== "string" || message.length > 4000) {
return new Response("Bad request", { status: 400 });
}
const response = await client.responses.create({
model: process.env.OPENAI_MODEL!, // 在服务端固定模型
input: message,
});
return Response.json({ reply: response.output_text }); // 只返回答案
}
为什么安全而不只是隐藏:
- 模型由服务端决定。 否则攻击者会选最贵的。
- 输入有上限。 不能通过你的账户发送超大提示词。
- 只返回答案。 不返回提供商原始响应或错误对象。
- 没有公开前缀。
OPENAI_API_KEY留在服务端;NEXT_PUBLIC_OPENAI_API_KEY会发给每个访问者。
Claude 和 Gemini 使用各自 SDK 与 ANTHROPIC_API_KEY 或 GEMINI_API_KEY。Google 的网站和 IP 限制有帮助,但任何有 curl 的人都能发送任意 Referer,所以当作第二层。浏览器调用唯一合理的场景是 bring-your-own-key 工具,每个用户粘贴自己的密钥。
让编码 Agent 用 Apifox CLI 测试代理路由
移动密钥只是一半;另一半是证明下一个功能上线后它仍在服务端。让构建路由的 Agent 用 Apifox CLI 测试。先配置:
npm install -g apifox-cli@latest
apifox login --with-token <YOUR_TOKEN>
npx -y skills add https://apifox.com # 按提示选择你的 Agent
给 Agent 一段提示:
Create Apifox test cases in project <id> for POST /api/chat on our staging
environment that assert:
1. a request without a session cookie returns 401,
2. a valid request returns 200 and the body does not contain "sk-", "sk-ant-",
"AIza" or "api_key",
3. the 21st request inside 60 seconds from the same user returns 429.
Run them and summarize any failures.
Agent 读取 schema、验证 payload、创建并运行:
apifox test-case category --project <id>
apifox cli-schema get test-case-create
apifox cli-schema validate test-case-create --file case.json
apifox test-case create --project <id> --file case.json
apifox run --test-case <caseId> --project <id> -e <envId> -r cli,json
第二条断言最关键:提供商错误对象或调试字段一旦回显密钥,它就会失败。把 apifox run ... -r junit 放进流水线,每次部署重新检查;Apifox CLI CI/CD 指南有配置,Claude Code 中的 Apifox CLI 则覆盖 Agent 侧。Apifox 免费版不限成员数量,小团队无需额外购买席位即可运行。
仓库侧的问题——密钥提交到 Git 而不是发到浏览器——见用密钥扫描器检查泄露。如果 Agent 调用处理客户数据的工具,测试 AI Agent 数据泄露覆盖下一层。
让密钥持续暴露的常见错误
- Base64 编码或拆分密钥。 浏览器必须重建才能发送,DevTools 会显示真实值。
- 移到 localStorage 或 cookie。 仍在客户端。
- 没有认证的代理。 公开
/api/chat就是记在你账单上的开放 AI 端点。 - “以防万一”保留旧密钥。 公开一天的密钥已被复制。
- 忘记移动应用。 Google 指南把 Web 和移动应用放在一起是有原因的。
常见问题
如何知道 OpenAI 密钥是否暴露? 打开 DevTools Network,触发 AI 功能。发往 api.openai.com 且带 Authorization: Bearer sk-... 的请求就是暴露。还要在脚本中搜索 sk- 和 dangerouslyAllowBrowser。
放在 NEXT_PUBLIC_ 或 VITE_ 变量中安全吗? 不安全。这些前缀就是为了把值复制到客户端 bundle。请用无前缀的仅服务端变量,并在服务端路由读取。
限制 Gemini 密钥到我的网站就安全吗? 有帮助,但浏览器外脚本可发送任意 Referer。Google 自己也建议客户端应用使用后端代理。
发现密钥暴露,先做什么? 创建新密钥,部署到服务端,再吊销旧密钥。然后把调用移到带认证和按用户限流的路由,并设置支出上限。
编码 Agent 能帮我修复并测试吗? 能。让它把调用移到服务端路由,再用 Apifox CLI 创建并运行测试:无会话返回 401、响应无密钥模式、超过限流返回 429。