browser-side-key-hygiene · ZH · 2026-10-23

在浏览器标签页里调聚合站而又不把 key 暴露出去

面向前端试验与内部工具的场景,讲清如何把聚合站 API key 留在薄中转层、浏览器只与中转层通信,同时保持 OpenAI 兼容 SDK 的调用写法不变。

问题:浏览器里放 key,等于公开 key

在浏览器标签页里直接调用 LLM API,最直接的做法是把 key 写进前端代码。问题在于,前端代码对使用者完全可见:打开开发者工具的 Network 面板就能看到请求头,构建产物里也能搜到字符串常量。即使做了混淆,也只是抬高一点门槛。

对聚合站这类一个 key 调多个模型的场景,泄露的代价更集中:一个 key 同时通向 Claude、GPT、DeepSeek、Qwen、GLM、Kimi 等多个后端,轮换一次要影响所有调用方。

所以结论很朴素:key 不进浏览器,浏览器只跟自己的中转层说话。

薄中转层的职责边界

中转层不需要是业务后端。它可以是一个很小的服务,只做三件事:

  • 持有聚合站 key,放在服务端环境变量里
  • 接收浏览器的请求,按需转发到聚合站的 OpenAI 兼容端点
  • 做一层身份校验,确认请求来自自己的页面或内部工具

它不应该做的:缓存业务数据、做计费逻辑、拼装 prompt 模板。中转层越薄,越不容易成为新的攻击面,也越容易替换。

保持 OpenAI 兼容的调用写法

关键点是:中转层对浏览器暴露的接口,形状上仍然是一个 OpenAI 兼容端点。这样前端代码可以继续用熟悉的 SDK,只改 base URL 和 key。

前端侧大致是这样:

const client = new OpenAI({
  baseURL: "/api/llm/v1",
  apiKey: await getSessionToken(), // 中转层签发的短期凭证
  dangerouslyAllowBrowser: true,
});

这里 apiKey 传的是中转层自己的会话凭证,不是聚合站的 key。请求路径、请求体结构、响应结构都保持 OpenAI 兼容,所以切换模型只是改 model 字段,前端逻辑不用动。

服务端侧做一次透明转发即可:把收到的请求体原样发往聚合站的 /v1/chat/completions,补上真正的 Authorization 头,再把响应流回传。流式响应要记得透传,不要在中转层里聚合成完整字符串再返回,否则首字延迟会明显变差。

凭证怎么发

中转层给浏览器的凭证,最好满足两点:短期、可撤销。

  • 内部工具可以用已有的登录态换取一个短期 token,随页面会话过期
  • 纯前端试验可以用同源 Cookie 加 CSRF 校验,避免 token 落到 localStorage

如果中转层和页面同源部署,还可以省掉跨域配置,减少一类出错点。不同源时记得只放行自己的来源,不要用通配。

为什么这个结构适合聚合站

聚合站本身的一个卖点是「一个 key 调多个模型」。这个特性在中转层结构下反而更顺:模型选择留在前端(改 model 字段即可),key 管理集中在服务端。换模型、加模型,前端不需要知道任何上游凭证。

充值侧也一样。用 Base 上的 USDC 充值、无 KYC,这些操作发生在账户层面,与浏览器里跑什么代码无关。按官方价 ×1.3 扣费的口径也只体现在账户余额上,前端不需要感知计费细节。

几个容易踩的坑

  • 忘了透传流式:中转层默认把 stream: true 的响应读完再返回,首屏等待会变长。用管道直接转发响应体。
  • 把错误信息原样透出:上游的错误体里可能带内部标识,中转层应做一次精简再返回。
  • 日志里记了完整请求体:prompt 里可能有敏感内容,日志只留元信息(模型、耗时、状态码)。
  • CORS 放开过头:只允许自己的来源,并且限制方法。
  • 凭证写死在构建产物:检查构建配置,确保没有把任何服务端变量注入到前端 bundle。

小结

把 key 留在薄中转层,前端仍然用 OpenAI 兼容 SDK 发请求,是浏览器场景下比较省心的做法。中转层只负责持有凭证、转发请求、校验来源,业务逻辑和模型选择留在前端。这样既保住了聚合站「一个 key 调多模型」的便利,又不用把凭证交给每一个打开的标签页。