在 GitHub 上搜索 "API keys" plaintext is:issue 吧。你会看到真实部署的多用户应用维护者写下类似“密钥目前以明文存储在数据库中,这对平台来说是责任”的句子。过去一个月我读过几十个这类 issue,因为我一直在给写这些 issue 的人发邮件。
需求侧的原因很容易解释。用户请求 BYOK(Bring Your Own Key,自带密钥),是因为他们已经在别处支付了 AI 费用,希望提示词在自己的服务商账户下运行,或者因为免费阶层的速率限制让他们感到烦躁。开发者想要 BYOK,是因为推理成本随使用量增长,而收入却跟不上。
于是 BYOK 不断被请求,也不断被糟糕地实现。以下是我在真实场景中看到的四个级别,从最差到生产级排序。
第 0 级:数据库中的明文列
这种做法比任何人愿意承认的都要常见。有一个 users.openai_api_key 列,保存时写入,每次请求时读取。
出现这种做法的原因可以理解:这个功能一下午就能跑起来。但一个服务商密钥并不等同于密码哈希。它是一个实时、可消费的凭证。如果你的数据库因备份泄露、配置错误的管理员面板或一次注入漏洞而泄露,攻击者拿到的不是需要破解的哈希,而是每个用户的可用密钥及其绑定的账单。
如果你今天正处于第 0 级,下面的加密列升级只需要一天的工作量。这周就去做吧。
第 1 级:环境变量
部署环境中的 OPENAI_API_KEY。对单租户场景完全没问题,适合自托管者和内部工具,操作者自己拥有密钥。一旦你面向托管且多用户场景,一个环境变量就意味着所有用户共享一个账户、一个速率限制和一张账单。这正是 BYOK 想要解决的情况。
第 2 级:密钥留在浏览器
使用 localStorage 或 IndexedDB,密钥在客户端附加到每次请求,服务器不存储任何内容。本地优先的工具采用这种方式,对它们来说这是正确的选择。你的服务器永远不会泄露它从未见过的东西。
当你的产品需要服务器端功能时,限制就显现出来了。后台任务、定时运行、Webhook、团队工作区、移动客户端——如果密钥只存在于一个浏览器标签页中,这些功能都无法实现。第 2 级对本地工具是可行方案,对托管 SaaS 则是死路。
第 3 级:服务端加密金库
这是托管、多租户产品的答案,也是实际工作量最大的地方。“加密密钥”听起来只是一行加密代码,但加密本身大概只有二十行,围绕它的才是产品。
以下是我认为生产级 BYOK 实现的最低检查清单:
加密
- 使用 AES-256-GCM,每次加密使用唯一 nonce,同一密钥下绝不重用 nonce。
- 加密密钥存放在数据库之外:KMS、Secrets Manager,或至少放在与数据不同的信任边界的环境变量中。
- 将密文绑定到其所在行(使用用户或连接 ID 作为 AAD),防止拥有数据库写入权限的攻击者在行之间交换密文。
用户体验规则
- 仅写入。保存后,客户端永远无法取回密钥。只显示
sk-...abc4这样的掩码。 - 更改密钥需要重新输入,而不是编辑。
运维卫生
- 仅在调用服务商的代码路径中、在请求时、在进程内解密。明文密钥绝不能出现在日志行、错误报告或分析事件中。上游服务商的错误有时会回显请求头,因此也要对这些进行脱敏。
生命周期(每个人都低估的部分)
- 撤销必须立即生效。用户点击断开连接后,只有正在进行的请求可以完成。
- 使用归因。当有人问“昨天哪个用户消耗了 40 美元”时,你需要给出答案,这意味着要按请求、按用户记录 token 数量。
- 轮换。用户会轮换服务商密钥,你的产品需要提供无需提交支持工单的轮换路径。
一个能捕获大多数错误实现的快速自测:你的支持人员能看到密钥吗?密钥是否出现在日志中?如果今晚数据库泄露,攻击者到底能拿到什么?用户能否一键终止访问?你能否知道哪个用户花费了多少?如果这些问题的答案让你感到不舒服,说明你还没做完。
购买选项
坦白我的立场:我自己构建了这样一个方案,所以本节的其余部分是推销。
Monet 是一个托管的服务端加密金库,形态类似 OAuth,因为用户已经信任这种授权流程。你的用户访问托管的连接页面,连接他们的 ChatGPT Plus 或 Claude Pro 账户,或粘贴自己的服务商密钥。凭证使用 AES-256-GCM 加密后存入金库,你的应用永远不会触碰它。你的应用通过标准的 authorization-code 交换获得一个不透明的 bearer token,并调用一个兼容 OpenAI 的端点:
from openai import OpenAI
client = OpenAI(
base_url="https://monet.gg/api/v1",
api_key=monet_access_token, # opaque token from the OAuth exchange, not a provider key
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)
Enter fullscreen mode Exit fullscreen mode
代理会将 token 解析为对应用户的凭证,流式返回响应,并写入一条包含服务商实际返回 token 数量的使用事件,因此计量和成本转嫁是免费的。撤销连接会清除金库行,token 也随之失效。服务端仅存储 token 的 SHA-256 哈希,因此即使 Monet 自己的 token 表被完整导出,也无法被消费。
公测期间免费。如果你想完整体验整个流程,可以访问 demo.monet.gg 的演示应用,开发者仪表盘位于 monet.gg。
如果你更想自己构建,上面的检查清单是诚实的最低要求,我也很乐意与你交流。过去一个月我读了太多明文密钥的 issue,希望能少一些。
相关阅读:我 15 岁,构建了“AI 订阅的 OAuth”。这是完整架构(托管方案的完整设计),以及 停止为用户支付 AI 使用费用(BYOK 的经济论据)。
我是 Shlok,15 岁,独立构建 Monet。我会阅读每一封回复:[email protected],github.com/shlok-madhekar,@shlokbuilds。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.