为什么绝不能在客户端代码中嵌入 Gemini API Key(以及 Firebase AI Logic 如何解决)
生成式 AI 的爆发,促使成千上万的 Web 开发者为应用添加智能功能。
人们的第一反应通常是在浏览器中直接调用 Gemini API 的 SDK。但这种直觉背后潜藏着一个严重的安全隐患:将 API 密钥暴露给全世界。
本文将深入解析为何向客户端传输原始 Gemini API 密钥极具危险性,介绍 Firebase AI Logic 的代理架构如何化解这一危机,并说明 Firebase App Check 如何弥补仅靠代理无法解决的剩余安全短板。
读完本文,你将获得一套可投入生产的完整配置:包含受保护的 AI Logic 客户端、正确配置的 App Check 流程(含调试令牌)、以及真实的用量统计、流式传输、多轮对话和结构化 JSON 输出实践,而不仅仅是单薄的 console.log。
目录
前置要求
开始之前,请确保具备以下条件:
Node.js v18 或更高版本(可通过
node --version查看)一个谷歌账号用于创建 Firebase 项目(Gemini 开发者 API 支持免费的 Spark 套餐)
熟悉 JavaScript、
async/await及 ES 模块代码编辑器和终端
无需具备 Firebase、App Check 或 Gemini API 的过往经验,本指南将从基础讲起,帮你逐步构建相关知识体系。
客户端 API 密钥的问题
把 API key 写进 JavaScript 打包文件,或者放进最终会被浏览器加载的 .env 文件,是一个严重的安全漏洞,而且利用起来毫不费力。下面看看实际场景是什么样的。
假设你在客户端代码里这样直接调用 Gemini API:
// DON'T do this in a browser-shipped app
const genAI = new GoogleGenerativeAI("AIzaSyD4-your-real-key-here");
用任何构建工具打包后,key 就会以明文形式出现在输出的 JS 文件里。任何人不需要特殊工具,一分钟内就能把它找出来:
# Anyone can run this against your deployed bundle
curl -s https://your-app.com/assets/main.js | grep -oE "AIzaSy[A-Za-z0-9_-]{33}"
只要 key 在里面,这一条命令就能从压缩过的生产环境打包文件里提取出 Gemini API key。而在浏览器开发者工具的 Network 面板里就更明显了:每个发往 generativelanguage.googleapis.com 的请求,key 都直接暴露在查询字符串或请求头里。
一旦你的 Gemini API key 这样泄露,攻击者可以:
耗尽你的全部调用配额
让你的 Cloud 账单不可预测地暴涨(Gemini 按调用 token 计费,不像数据库读取那样是固定费率)
用你的资源跑他们自己的请求,可能导致你的 Google Cloud 项目因滥用被停用
过去唯一的解决办法是搭建、部署并维护一个自定义后端服务(Node.js、Python、Go……),作为应用和 Gemini API 之间的代理,只为保住一个字符串的安全。本该很简单的一个功能,却要为此维护真正的基础设施。
第一步 – Firebase AI Logic 的代理架构是如何工作的
Firebase AI Logic 直接提供了这种代理网关,你无需自己搭建或托管。你依然写客户端代码,但 key 永远不会离开 Google 的基础设施。
[Web Browser] ──(Authenticated Request)──> [Firebase AI Logic Proxy] ──(Key Injected Server-Side)──> [Gemini API]
你的 Gemini API key 安全地存储在 Firebase 项目里。客户端 SDK 把请求发给代理网关,由代理注入 key,再转发给你选择的 Gemini API 提供方。key 绝不会出现在你的 JS 打包文件、网络请求或任何浏览器能检查到的东西里。
Firebase AI Logic 支持两种服务提供商,需要在控制台中配置服务时进行选择:
| Gemini Developer API | Agent Platform Gemini API(原 Vertex AI) | |
|---|---|---|
| 计费方案 | 兼容免费 Spark 方案 | 需使用 Blaze(按量付费)方案 |
| 适用场景 | 快速启动、原型开发、大多数 Web/移动应用 | 数据驻留要求、已使用 Google Cloud/Vertex AI 的团队 |
| 区域控制 | 受限 | 可根据模型选择特定区域(us、eu)访问模型 |
| 配置复杂度 | 极低,无需绑定账单 | 需关联 Cloud Billing 账户 |
对于大多数应用,建议从 Gemini Developer API 开始,本教程也采用此方式。后续切换服务商只需更改配置,无需重写代码,因为两者均通过同一个 getGenerativeModel() 接口调用。
步骤 2 – Firebase App Check 的实际作用
代理层可以隐藏密钥,但这本身并不能阻止任何人调用该代理。你的 Firebase 配置对象(如 apiKey、projectId 等)并非机密,它们本就设计为公开可见,并存在于每个已部署应用的文件包中。如果没有额外的防护层,机器人可以复制该配置,初始化一个指向你项目的独立 Firebase 应用,直接调用你的 AI Logic 代理,从而在没有任何真实用户参与的情况下推高你的 Gemini 账单。
这正是 Firebase App Check 的用武之地。理解其具体功能至关重要,因为它容易被与身份验证混淆。
App Check 是认证(attestation),而非身份验证(authentication)。 Firebase Authentication 回答的是“这是哪个用户?”,而 App Check 回答的是另一个问题:“这个请求是否来自我应用的真实、未被篡改的实例,而不是脚本、机器人或其他使用了我配置的应用?”你可以且应该同时使用两者,但当请求不包含任何用户上下文时,App Check 是保护你免受滥用的关键防线。
流程的具体步骤如下:
应用初始化 App Check 时,SDK 会向配置的服务商发起认证挑战。在 Web 端,这对应 reCAPTCHA Enterprise:它执行隐式风险评估(检测鼠标移动、浏览器指纹、网络信号等),并返回一个令牌,声明“此会话看起来是合法的浏览器会话”。
应用 SDK 将获得的 reCAPTCHA 令牌发送至 Firebase 的 App Check 后端,换取一枚Firebase App Check 令牌,即短时有效且经过签名的 JWT。
该 App Check 令牌会在本地缓存,并自动附加到应用随后向 Firebase AI Logic 及其他集成 App Check 的服务(如 Firestore、Cloud Functions)发出的所有请求中。
AI Logic 代理在将请求转发给 Gemini 之前,会验证 App Check 令牌的签名与有效性。若无有效令牌,请求根本无法触达模型。
对于生成式 AI 而言,这一点尤为关键,比传统的 CRUD 后端更甚,核心在于成本结构。被拦截的 Firestore 读取请求不产生费用;而一旦未被拦截的 Gemini 调用,每次请求都可能产生真实费用,且在大流量场景下,脚本化的滥用循环可能在数小时内耗尽月度预算。App Check 正是那道确保仅真实应用能触发此类支出的闸门。
提示:Google 已宣布,自 2026 年 11 月 2 日起,Firebase AI Logic 将强制启用 App Check。更早的时间点,2026 年 7 月起,Firebase 控制台中的引导式配置流程已为新的 AI Logic 集成自动开启 App Check。强制执行日期后,所有未经验证的请求将被直接拒绝,因此建议现在就配置好,以免日后手忙脚乱。
第 3 步 – 配置 Firebase 项目
前往 Firebase 控制台,创建新项目(或打开现有项目)。
在左侧边栏点击 Build,然后选择 AI Logic,再点击 Get started。选择 Gemini Developer API 作为服务商,即可无需配置账单直接跟随操作。
回到Project settings,进入General,再打开Your apps。如果还没有注册 Web 应用,就先注册一个,然后复制 Firebase 配置对象,后面的步骤会用到。
App Check 的配置我们留到下一步单独讲,因为它值得详细介绍。
第 4 步 – 集成 Firebase App Check
为 reCAPTCHA Enterprise 注册你的应用
在 Firebase 控制台中,进入Build下的App Check,选中你的 Web 应用,并将reCAPTCHA Enterprise设为提供商。Firebase 会生成一个与你应用域名绑定的site key,复制下来,稍后会传入代码中。
安装 SDK
npm install firebase
你只需要这一个包。ReCaptchaEnterpriseProvider 包含在 firebase/app-check 中,而它是 firebase 包的一部分,无需单独安装 reCAPTCHA SDK,也不用手动添加 <script> 标签。只要 initializeAppCheck() 一运行,Firebase 就会自动加载 reCAPTCHA Enterprise 脚本。
本地开发时用 Debug Token 初始化 App Check
reCAPTCHA Enterprise 在 localhost 上表现不可靠,所以写生产代码之前,先配置 App Check 的debug provider,这样本地开发就不会被误拦截:
// app-check-setup.js
import { initializeApp } from "firebase/app";
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from "firebase/app-check";
const firebaseConfig = {
apiKey: "AIzaSy...",
authDomain: "your-project.firebaseapp.com",
projectId: "your-project",
storageBucket: "your-project.firebasestorage.app",
messagingSenderId: "123456789",
appId: "1:1234:web:abcd",
};
const app = initializeApp(firebaseConfig);
// 只在本地/开发环境启用 debug provider。
// 首次运行时会向控制台打印一个 debug token。
if (location.hostname === "localhost") {
self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}
export const appCheck = initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider("YOUR_RECAPTCHA_ENTERPRISE_SITE_KEY"),
isTokenAutoRefreshEnabled: true,
});
export { app };
本地首次运行后,在浏览器控制台里找一行类似这样的内容:
应用检查(App Check)调试令牌:5f2b1a3c-....-....-.... 在令牌生效前,需将其添加至 Firebase 控制台中的应用 App Check 设置。
将该令牌复制并粘贴到控制台的 App Check → 应用 → [你的应用] → 管理调试令牌 中。此后,来自本地机器的请求将视为已验证,无需通过真实的 reCAPTCHA 挑战。切勿将此调试令牌配置块发布到生产环境。应如上文所示,通过环境检查来限制其使用。
验证是否生效
当应用发起第一个受 App Check 保护的请求时(你将在第 5 步中实现该功能),前往控制台的 App Check 然后进入 APIs。你将看到 Firebase AI Logic 接收到的已验证与未验证请求的实时统计。如果配置正确,你的流量将显示为“已验证”。
步骤 5 – 实现 Firebase AI Logic
配置好 App Check 后,以下是实际使用该模型的方法,而不仅仅是单次请求/响应交互。
基本设置:
// ai-client.js
import { getAI, GoogleAIBackend, getGenerativeModel } from "firebase/ai";
import { app } from "./app-check-setup.js";
// useLimitedUseAppCheckTokens 会颁发短期令牌,在标准 App Check 验证之上提供针对重放攻击的额外保护。
const ai = getAI(app, {
backend: new GoogleAIBackend(),
useLimitedUseAppCheckTokens: true,
});
export const model = getGenerativeModel(ai, { model: "gemini-3.8-flash" });
注意:Gemini 模型名称和可用性经常变更,gemini-2.0-flash 及其 Lite 变体已于 2026 年 6 月 1 日退役,Gemini 2.5 系列现已被弃用,转而支持 Gemini 3.x 系列。
在生产环境中硬编码模型名称之前,请务必查阅 支持的模型页面。或者,更好的做法是从 Firebase Remote Config 加载模型名称,以便无需发布新构建即可切换模型。
示例 1 — 单次请求
import { model } from "./ai-client.js";
async function generateAIText(prompt) {
try {
const result = await model.generateContent(prompt);
const response = result.response;
return response.text();
} catch (error) {
console.error("Firebase AI Logic 请求失败:", error);
}
}
generateAIText("用两句话解释 API 代理的作用。");
示例 2:流式渲染到界面
对于稍长的内容,流式输出能让用户立即看到响应开始生成,避免数秒的空白等待。下面是一个真正操作 DOM 的示例,而不仅仅是控制台打印:
import { model } from "./ai-client.js";
async function streamIntoElement(prompt, targetElement) {
targetElement.textContent = "";
const result = await model.generateContentStream(prompt);
for await (const chunk of result.stream) {
targetElement.textContent += chunk.text();
}
// 流式传输结束后,也可以获取聚合后的最终响应
const finalResponse = await result.response;
console.log("Total tokens used:", finalResponse.usageMetadata?.totalTokenCount);
}
const output = document.querySelector("#ai-output");
streamIntoElement("Write a 3-sentence product description for a smart water bottle.", output);
示例 3:多轮对话
在开发聊天机器人功能时,你不需要手动记录并每次重新发送整个对话历史。startChat() 会自动帮你处理这些:
import { model } from "./ai-client.js";
const chat = model.startChat({
history: [
{ role: "user", parts: [{ text: "I'm building a task management app." }] },
{ role: "model", parts: [{ text: "Got it, what would you like help with?" }] },
],
});
async function sendChatMessage(message) {
const result = await chat.sendMessage(message);
return result.response.text();
}
sendChatMessage("Suggest 3 status labels for a Kanban board.");
// 后续调用会自动携带之前的对话作为上下文
sendChatMessage("Now suggest color codes for each of those.");
示例 4:结构化 JSON 输出
如果你要把模型的输出接入应用逻辑(比如渲染卡片或填充表单),自由文本格式很难稳定解析。传入 responseSchema 可以强制模型返回有效且类型明确的 JSON:
import { getGenerativeModel, Schema } from "firebase/ai";
import { ai } from "./ai-client.js"; // 假设 `ai` 也从 ai-client.js 导出
const taskSchema = Schema.object({
properties: {
title: Schema.string(),
priority: Schema.enumString({ enum: ["low", "medium", "high"] }),
tags: Schema.array({ items: Schema.string() }),
},
});
const structuredModel = getGenerativeModel(ai, {
model: "gemini-3.8-flash",
generationConfig: {
responseMimeType: "application/json",
responseSchema: taskSchema,
},
});
async function extractTaskFromText(text) {
const result = await structuredModel.generateContent(
`Extract a task from this note: "${text}"`
);
return JSON.parse(result.response.text());
}
extractTaskFromText("Need to review the PR from Sarah by Friday, this is urgent");
// → { title: "Review PR from Sarah", priority: "high", tags: ["review"] }
示例 5 —— 处理速率限制和瞬时错误
调用 Gemini 时可能会碰到速率限制或瞬时故障,高负载下尤其如此。用简单的指数退避(exponential backoff)就能让应用保持健壮,又不会频繁轰炸 API:
import { model } from "./ai-client.js";
async function generateWithRetry(prompt, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await model.generateContent(prompt);
return result.response.text();
} catch (error) {
const isRetryable = error.message?.includes("429") || error.message?.includes("503");
if (!isRetryable || attempt === maxRetries) throw error;
const delayMs = 2 ** attempt * 1000; // 1秒、2秒、4秒……
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
常见问题排查
问题 1:API key not valid. Please pass a valid API key.
这通常说明 Firebase 配置里的 apiKey 与你的项目不匹配,或者所需 API 没有启用。请在控制台的 Project settings → General 中核对配置值。
问题 2:代码看起来没问题,却返回 403 或请求被静默拦截
大多数情况下,这是 App Check 注册配置不匹配的问题。请确认测试所用的域名与 reCAPTCHA Enterprise 站点密钥注册的信息一致,并在控制台的 App Check 页面下找到 APIs,确认请求状态显示为“Verified”(已验证),而非“Unenforced”(未强制执行)或完全缺失。
问题 3:App Check 在生产环境正常,但在 localhost 上失败
这是预期行为,reCAPTCHA Enterprise 在 localhost 上无法稳定运行。请确保开发环境中启用了第 4 步提到的调试令牌代码块,并且已将打印出的调试令牌添加到控制台的 App Check 下的 Manage debug tokens 中。如果更换了机器或清除了浏览器存储,系统会生成新的令牌,此时需要重新注册。
问题 4:模型报错或意外中断
Google 会定期退役旧的 Gemini 模型,并提前数月通知。例如,gemini-2.0-flash 及其 Lite 版本已于 2026 年 6 月 1 日停止服务。如果之前正常的请求突然返回 404 错误,请先查看 模型页面 上是否有废弃公告,再怀疑是代码 bug。
问题 5:流式传输中途停止且无错误信息
这通常是因为触发了 usageMetadata 或令牌限制,而非网络故障。请检查 finalResponse.candidates[0].finishReason(在 result.response 解析后即可查看),若值为 MAX_TOKENS,说明响应因长度限制被截断,而非系统崩溃。
结语
构建生成式 AI 功能,意味着从原型开发阶段就要采用生产级安全标准,而非事后补救。Firebase AI Logic 的代理架构彻底将 Gemini API 密钥隔离在客户端代码之外,而 Firebase App Check 则确保即使密钥隐藏,只有应用的有效实例才能消耗配额。两者结合,覆盖了最关键的两类安全风险:密钥泄露和脚本滥用。
接下来值得进一步探索的几个方向:
函数调用 / 工具使用,让 Gemini 在响应过程中直接调用应用内的自定义函数
服务端提示模板,若希望将提示词也完全隔离在客户端之外,而不仅仅隐藏 API 密钥
混合推理:在支持的浏览器中,若可用则回退至端侧模型,从而降低简单请求的成本和延迟
通过 Firebase Remote Config 管理模型名称:无需发布新版应用,即可向用户推送新的 Gemini 模型