文档 / 搜索 / LingCode Cloud / 密钥保险库
📘 参考 ● 安全 更新于 2026-06-11

密钥保险库

一句话:密钥保险库保存你的应用需要、但绝不能下发到浏览器的私有密钥——Stripe secret key、Resend / Twilio / ElevenLabs 的 API key,以及任何第三方凭证。你把每一个都用环境变量风格的 NAME 存储一次;你的函数在运行时于服务端以 ctx.secrets.NAME 读取它。浏览器永远看不到这个值。密钥用 AES-256-GCM 静态加密,主密钥存放在服务器环境里(而非数据库),控制台只列出名称——值是写入即不可回读的。

这和 anon key(匿名密钥)互为对照。anon key 是设计上就公开的——它会出现在你的客户端打包产物里,由行级安全负责守门。密钥则恰恰相反:它们正是那些必须保持私密的东西,而保险库的存在就是为了让这个值只活在服务端,由函数使用。一旦你发现自己想把厂商的 secret key 放到浏览器够得着的地方,答案几乎总是「把它放进保险库,改从函数里读取」。

为什么需要一个保险库

大多数应用最终都会用到一个不能暴露的凭证。要扣款,你需要你的 Stripe secret key。要发送事务性邮件,你需要一个 Resend key。要发短信,你需要一个 Twilio token;要合成语音,需要一个 ElevenLabs key。这些都不能下发到浏览器——任何打开开发者工具的人都能拿到它们,进而花你的钱、或冒充你发邮件。

保险库通过给每个私有密钥在服务器上安一个家来解决这个问题。你存储它一次,你的服务端代码在运行时读取它,它永远不会越过线路传到客户端。这就是全部的思路:这个值只存在于一个浏览器看不见的地方。

它如何工作

每个密钥都以环境变量风格的 NAME(大写字母、数字和下划线——例如 STRIPE_SECRET_KEY)和一个 value(值)存储。这个值用 AES-256-GCM 静态加密。加密主密钥存放在服务器环境里,而非数据库中,这带来两个值得记牢的后果:

限制

每个后端最多 32 个密钥。每个值最大 16 KB。名称必须匹配 [A-Z][A-Z0-9_]{0,63}——以一个大写字母开头,后面再跟最多 63 个大写字母、数字或下划线。

设置密钥

密钥在 Cloud 控制台里管理——仅限所有者——位于 lingcode.dev/backends.html → 你的后端 → Secrets。在那里你可以添加更新删除密钥。

有一点要提前知道:列表只会显示名称。一旦存储,值就永远不会被返回——你可以覆盖一个密钥,但无法把它读回来。如果你忘了某个值是什么,重新设置它,而不是试图把它找回来。

底层是一组以所有者身份认证的端点,方便你对它们写脚本:

GET    /api/cloud/account/backends/<backend-id>/secrets          # 仅列出名称(NAMES)
PUT    /api/cloud/account/backends/<backend-id>/secrets/<KEY>    # 添加或更新
DELETE /api/cloud/account/backends/<backend-id>/secrets/<KEY>    # 删除

# PUT 请求体:
{ "value": "sk_live_..." }

从函数里读取密钥

设置密钥只完成了一半——重点是用上它。密钥在服务端从函数里读取,而不在其他任何地方。这有三种展开方式。

内置函数(Builtins)

每个内置函数都声明它需要哪些密钥名称,然后在你调用时于服务端读取它们。你设置密钥,再调用内置函数——你从不自己传入这个 key。例如:

所以收一笔款的端到端闭环是:在控制台里设置 STRIPE_SECRET_KEY,然后调用 stripe-checkout 内置函数。内置函数在服务器上读取这个密钥;你的客户端只会看到它返回的 checkout URL。

自定义函数

如果你编写自己的自定义函数,密钥会作为 ctx.secrets 出现在处理函数的上下文里:

export default async function handler(input, ctx) {
  const key = ctx.secrets.STRIPE_SECRET_KEY;
  // 用 key 调用厂商 API ……
  return { ok: true };
}

和内置函数是同一套模型,只是改成手写:你在控制台里设置 STRIPE_SECRET_KEY,当函数在服务器上运行时,它就以 ctx.secrets.STRIPE_SECRET_KEY 的形式出现。这个值永远不会离开沙箱。

http-fetch 内置函数

http-fetch 内置函数可以用 {{SECRET_NAME}} 占位符语法,把一个密钥插值进请求头或请求体。这样你就能向任意一个在白名单内的 API 发起带认证的请求——完全无需编写自定义函数:

{
  "url": "https://api.example.com/v1/things",
  "method": "GET",
  "headers": {
    "Authorization": "Bearer {{EXAMPLE_API_KEY}}"
  }
}

{{EXAMPLE_API_KEY}} 占位符会在请求发出之前于服务端替换成密钥的值。浏览器发送的是请求的形状,而非这个 key。

保险库保护什么、不保护什么(v1,实话实说)

保险库防范两类具体威胁。它抵御数据库外泄——主密钥不在数据库里,所以一份被盗的导出不会交出你的密钥。它也抵御篡改——GCM 认证标签意味着被修改过的密文会解密失败,而不是返回一个被破坏的值。

它在 v1 里不做的事:存在一个单一的部署级主密钥。没有按用户派生密钥,也没有内置的密钥轮换。把保险库当作厂商密钥的正确归宿——它确实是——而如果某个 key 泄露了,请到厂商那边轮换它(签发一个新的 Stripe key、吊销旧的),再到控制台里更新这个密钥。

永远不要把私有密钥放进客户端代码、放在 anon key 够得着的地方,或放进公开表里。如果浏览器能读到它,它就是公开的——不管它是怎么跑到那儿去的。密钥之所以存在,正是为了让这个值只活在服务端、由函数使用。当你忍不住想「先硬编码上去顶一下」时,请改成把它设进保险库;工作量一样,而它会保持私密。