← 文章 / AI技术
freeCodeCamp 1小时前 · 2026-09-21 23:56:25 · 3 阅读

为什么绝不能在客户端代码中嵌入 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 的团队
区域控制 受限 可根据模型选择特定区域(useu)访问模型
配置复杂度 极低,无需绑定账单 需关联 Cloud Billing 账户

对于大多数应用,建议从 Gemini Developer API 开始,本教程也采用此方式。后续切换服务商只需更改配置,无需重写代码,因为两者均通过同一个 getGenerativeModel() 接口调用。

步骤 2 – Firebase App Check 的实际作用

代理层可以隐藏密钥,但这本身并不能阻止任何人调用该代理。你的 Firebase 配置对象(如 apiKeyprojectId 等)并非机密,它们本就设计为公开可见,并存在于每个已部署应用的文件包中。如果没有额外的防护层,机器人可以复制该配置,初始化一个指向你项目的独立 Firebase 应用,直接调用你的 AI Logic 代理,从而在没有任何真实用户参与的情况下推高你的 Gemini 账单。

这正是 Firebase App Check 的用武之地。理解其具体功能至关重要,因为它容易被与身份验证混淆。

App Check 是认证(attestation),而非身份验证(authentication)。 Firebase Authentication 回答的是“这是哪个用户?”,而 App Check 回答的是另一个问题:“这个请求是否来自我应用的真实、未被篡改的实例,而不是脚本、机器人或其他使用了我配置的应用?”你可以且应该同时使用两者,但当请求不包含任何用户上下文时,App Check 是保护你免受滥用的关键防线。

流程的具体步骤如下:

  1. 应用初始化 App Check 时,SDK 会向配置的服务商发起认证挑战。在 Web 端,这对应 reCAPTCHA Enterprise:它执行隐式风险评估(检测鼠标移动、浏览器指纹、网络信号等),并返回一个令牌,声明“此会话看起来是合法的浏览器会话”。

  2. 应用 SDK 将获得的 reCAPTCHA 令牌发送至 Firebase 的 App Check 后端,换取一枚Firebase App Check 令牌,即短时有效且经过签名的 JWT。

  3. 该 App Check 令牌会在本地缓存,并自动附加到应用随后向 Firebase AI Logic 及其他集成 App Check 的服务(如 Firestore、Cloud Functions)发出的所有请求中。

  4. 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 项目

  1. 前往 Firebase 控制台,创建新项目(或打开现有项目)。

  2. 在左侧边栏点击 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 模型

    原始来源: freeCodeCamp

    评论 (0)