FLUX 的 API 密钥应当放在服务端,不要写进任何会被浏览器下载或执行的代码里。判断标准很直接:只要某个值出现在前端源码、构建产物,或者浏览器发出的请求里,它就已经是公开信息,换密钥、加混淆只是拖延被发现的时间。接入时比较稳的做法是自己加一层转发接口,把手里的调用接出去,前端只发业务参数。
密钥只放服务端:前端能读到的任何变量,等于已经公开。做法是加一层自己的转发接口,密钥从服务端环境变量读入,前端只发业务参数;验证方式有两个——在 DevTools 里搜密钥前缀看有没有命中,对构建产物目录做全文检索。边界在于这层转发不解决鉴权与限流,上游错误照原样回传,日志不记密钥本身。
看清一次请求从浏览器到模型的完整路径
把一次调用拆成三段,密钥该放哪儿就清楚了:
浏览器页面 JS
│ 只带业务参数 + 调用方标识(不含密钥)
▼
你自己的服务端(密钥从环境变量读入,只存在于进程内存)
│ 在服务端拼上 Authorization,发起上游请求
▼
FLUX / 模型服务
密钥只允许出现在第二段:服务端进程读到的环境变量,以及服务端发往模型服务的那条出站请求。第一段和第三段之间的所有内容,用户都能通过开发者工具看到。
具体到可能泄露的位置,通常是三处:
- 源码:前端仓库里写死了密钥常量,或者某个被前端构建读取的配置文件里带了密钥。
- 构建产物:打包时把变量内联进 JS,dist / build 目录里能直接搜到这个字符串。
- 网络请求:浏览器自己发出的请求头或请求体里带着密钥。
这三处只要有一处命中,就等于密钥公开。有些前端构建工具只把特定前缀的变量注入客户端代码,这恰好说明:任何能被前端读到的变量名,都该当成公开值对待。前端需要知道的只有一件事——转发接口的地址。
把密钥放进服务端环境变量,不写进任何前端文件
服务端读密钥的通用写法是只读环境变量,不给兜底默认值:
// server.js
const API_KEY = process.env.FLUX_API_KEY;
if (!API_KEY) {
throw new Error("FLUX_API_KEY 未配置,拒绝启动");
}
给默认值(比如空字符串)容易把配置错误拖到线上才暴露,建议启动阶段就失败,早失败比线上静默报错好排查。
本地和线上用两套取值方式。本地把密钥写进 .env,用 dotenv 之类的方式加载,并且把 .env 加进 .gitignore;仓库里只提交一份 .env.example,内容写占位符。线上用部署平台的环境变量注入,不要把 .env 文件打进镜像或者留在服务器目录里。
# .env(本地,不提交版本库)
FLUX_API_KEY=替换成你自己的密钥
# .env.example(提交,给协作者看字段名)
FLUX_API_KEY=your-key-here
# 本地启动:加载 .env 后运行
node -r dotenv/config server.js
# 线上启动:环境变量由平台注入,直接运行
node server.js
还有一点:前端的构建配置里不要把这个变量名暴露成客户端可见的变量。命名上建议把服务端专用变量和前端公开变量区分开,避免以后有人顺手把它加进前端可读列表。
写一个最小转发接口,只透传必要字段
转发接口只做三件事:校验输入、补上密钥、把上游响应原样返回。路径和字段名按你自己的项目定,下面是骨架。
// 前端发来的请求体(不含密钥)
POST /api/generate
{
"prompt": "...",
"options": { } // 需要透传的其它参数,走白名单校验
}
// 成功响应:回传上游结果
{
"ok": true,
"data": { }
}
// 出错响应:保留上游状态码与错误体
{
"ok": false,
"status": 401,
"error": { }
}
import express from "express";
const app = express();
app.use(express.json({ limit: "1mb" }));
const UPSTREAM_URL = process.env.FLUX_UPSTREAM_URL; // 按实际接口填
app.post("/api/generate", async (req, res) => {
const { prompt, options } = req.body ?? {};
if (typeof prompt !== "string" || !prompt.trim()) {
return res.status(400).json({ ok: false, error: "prompt 缺失" });
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 60000); // 超时上限,按业务调整
try {
const upstream = await fetch(UPSTREAM_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FLUX_API_KEY}`,
},
body: JSON.stringify({ prompt, ...pickAllowed(options) }),
signal: controller.signal,
});
const text = await upstream.text();
res.status(upstream.status)
.type("application/json")
.send(text);
} catch (err) {
const status = err.name === "AbortError" ? 504 : 502;
res.status(status).json({ ok: false, error: String(err) });
} finally {
clearTimeout(timer);
}
});
两个地方容易忽略。一是 options 用白名单过滤,不要把手传来的任意字段直接拼进上游请求,否则用户能借转发接口传别的参数。二是上游错误尽量连状态码带响应体一起返回,前端才能区分是参数问题、鉴权问题还是配额问题。如果上游返回的是流式内容,转发时要逐块转发,不要先攒完整包再回,否则长任务会先卡在超时上。另外,出错分支里不要把密钥拼进错误信息返回给前端。
在浏览器里验证密钥确实没出现
改完之后,建议做两个可以观察的验证动作。
动作一:看 Network 面板。打开 DevTools,触发一次完整调用,逐条看请求。重点看两类:浏览器发给你自己服务端的请求,它的 Headers 和 Payload 里应该只有业务参数和调用方标识;资源加载列表里应该不出现模型服务的域名,出现了说明还有前端直连。然后在 Network 面板的过滤框里输入密钥的前几位,没有命中条目才算通过。
动作二:检索构建产物。先完整构建一次,再在产物目录里搜字符串:
grep -rIn "密钥的前 8 位" dist/ build/ out/ 2>/dev/null
# 没有输出即为未命中;有输出就顺着文件名回去改构建配置
顺手补两项检查:在源码仓库里搜同样的字符串,确认没有写死;用 git log `--all` -- .env 看有没有误提交过密钥文件。检索的是密钥字符串本身,所以不要把完整密钥贴进任何文件或 shell 历史,只取前几位做局部匹配就够定位了。构建产物里的 source map 同样会被下载,也在检索范围内。
给转发层加上按次记录
转发层是唯一知道调用方和真实调用时机的地方,按次记录能给后面的出图量估算和异常排查留依据。每条记录建议包含这几项:
{
"ts": "2026-01-01T00:00:00Z", // 服务端时间,ISO 格式
"caller": "调用方标识", // 登录用户 ID / 会话 ID / 匿名标识
"status": 200, // 上游返回的状态码
"latency_ms": 1234 // 从发出上游请求到收到响应的耗时
}
字段的取舍说明:时间用服务端时钟,不要采信前端传来的时间;调用方标识用于定位是谁在大量调用,匿名场景至少给一个服务端生成的会话标识;status 记上游返回的状态码,便于把鉴权失败和参数错误分开看;latency_ms 从转发层发出请求算到收到响应,用来找慢请求。
明确不记录密钥本身,也不记完整的 Authorization 头;如果日志里要带 prompt 或图片链接,先确认是否含用户隐私再决定记不记。落地可以先写文件或标准输出,需要时再接日志服务,先保证每条调用都能对上号,比一开始就上完整方案更实用。