r3gBlog

Back

在 MaaS(Model as a Service,模型即服务)产品的开发里,有一个绕不开的接入方式:用户把自己在 OpenAI、Anthropic 等模型供应商处申请的 API Key 填进产品,让平台替他调用模型。这种做法叫 BYOK(Bring Your Own Key,自带密钥)。Key 第一次从前端传到后端,之后由后端持有、替用户调用第三方模型。这一步传输入口,就是加解密需求出现的地方。

传 Key 有安全隐患,这一点没有争议:密钥在链路上明文流转、落库明文保存、被日志打印出来,任何一个环节泄露都是事故。于是「给前后端传 Key 做加解密」被提上日程。这个需求真正的起点,是先拆清楚 Key 的旅程:从输入框一路走到第三方模型服务,中间要经过哪些层,每一层谁可能看到它,加解密到底能挡住哪一层。

这篇文章按威胁模型把主流方案分层梳理:传输层的 TLS 与应用层混合加密、存储层的 KMS 信封加密、架构层的后端代理与短时令牌、运维层的脱敏与轮换。每个方案先说它防什么,再讲怎么做,最后交代边界。结论先行:加解密各管一段,真正决定安全边界的,是密钥流动的方向。

先拆威胁模型:Key 会经过谁#

把一次 BYOK 接入里 Key 的旅程完整走一遍,每一站都是潜在泄露点:

环节谁能看到风险
用户在输入框粘贴 Key浏览器内存XSS、浏览器插件、恶意脚本可以读走
浏览器发出请求网络链路网络嗅探与中间人,由 TLS 挡住
TLS 终止点(CDN / WAF / 反向代理)云厂商网关、运维人员TLS 之后进入明文域,网关日志可能记录请求体
后端进程后端代码、日志、错误上报一行 console.log 就把 Key 打出去
数据库数据库泄露、备份泄露明文落库等于把全部用户的 Key 放在同一个篮子里
后端再次读出 Key 调用第三方后端内存、第三方侧日志调用链路上 Key 再次出现在内存

这六站里,加解密能覆盖的只有其中几站。中间的网络、网关、日志、数据库,都可以用加密来挡住;但两端的端点防不住——浏览器这一端,Key 是用户自己输入的,浏览器内存里本来就有一份明文;后端这一端,后端进程必须有解密能力才能用这个 Key,所以拿到了进程权限也就拿到了 Key。

浏览器端这一点值得单独说透。任何运行在用户设备上的代码,用户(以及潜在的攻击者)都能检查。这是 OAuth 术语里 public client(公开客户端)和 confidential client(机密客户端)的分界线:浏览器属于前者,你无法控制它的运行环境,里面藏的密钥都可以被提取;服务器属于后者,用户可以访问的只有接口,碰不到进程内存和文件系统。前端打包时把 Key 塞进 VITE_ 之类的构建变量,只是把明文写进了 JS bundle,DevTools 里一翻就出来;混淆也挡不住,这是所有 DRM 都失败过的同一个原因。

加解密能防住中间的每一层,防不住端点本身。Key 一旦进了浏览器,就没有真正的秘密可言。

主流的几种加解密方案#

抛开层次,先认识加解密工具箱里的四种方案:对称加密、非对称加密、混合加密,以及国内强合规场景绕不开的国密算法。

对称加密:一个密钥管加解密#

对称加密用同一个密钥加密和解密,代表是 AES(Advanced Encryption Standard,高级加密标准),常用 AES-256-GCM 模式——GCM 在加密的同时输出一段认证标签,能发现密文被篡改。它的优点是快、没有明文长度限制;代价是密钥分发:加密和解密两端必须持有同一个密钥,而在前后端场景里,这个共享密钥一旦存在浏览器里,就逃不掉「浏览器里的一切都可被提取」。所以对称加密很少单独承担浏览器到后端的传输加密,它更常出现在两端都能安全持有密钥的地方:存储加密、服务间内部调用、本地密钥库。

非对称加密:一对钥匙,公钥随便给人#

非对称加密用一对密钥,公钥加密、私钥解密,公钥可以公开分发,私钥由持有方保密。它解决了对称加密的密钥分发难题——前端拿着公钥加密、后端拿私钥解密,两端不需要预先共享任何秘密。代价是两点:一是非对称运算慢;二是 RSA-OAEP 对明文长度有硬上限,2048 位密钥配 SHA-256 时一次最多加密 190 字节。另一个主流非对称算法是 ECDH(Elliptic Curve Diffie-Hellman,椭圆曲线密钥交换),它本身不做加密,而是让双方交换各自的临时公钥、各自独立算出同一个共享密钥,再配合对称加密使用,特点是前向保密——会话用的密钥是一次性的,用完作废。

混合加密:非对称护送钥匙,对称加密数据#

混合加密(数字信封)把前两样拼起来:用非对称加密护送一个随机生成的对称密钥,用这个对称密钥加密真正的数据。它同时绕开了非对称的速度与长度限制,又不需要预先共享密钥,是「浏览器 → 后端」传输加密的标准形态。两种主流组合是 RSA-OAEP + AES-GCM 和 ECDH + AES-GCM。标准化的封装格式有 JWE(JSON Web Encryption,JSON 网络加密),把算法标识、密文、密钥材料打包成统一结构,两端按规范即可互解。

国密:强合规行业的指定算法#

国密是国家密码管理局发布的中国商用密码标准:SM2 是非对称算法(对标 RSA / ECDH),SM4 是分组对称密码(对标 AES),SM3 是哈希算法(对标 SHA-256)。它在政企、金融,以及需要等保测评、商用密码应用安全性评估(密评)的行业里是强制要求。关键限制是浏览器原生不支持:Web Crypto API 没有 SM2 / SM4 的实现,前端做国密要引入 sm-crypto 这类第三方库,或者把加解密收敛到后端由网关代转。国内云厂商的 MaaS 一旦面向强合规行业客户,方案里就要为国密留出口。

四种方案的定位#

方案密钥分发速度明文长度限制浏览器原生支持典型用途
对称加密(AES-256-GCM)需预先共享密钥快无有存储加密、服务间调用
非对称(RSA-OAEP)公钥公开即可慢有(2048 位约 190 字节)有前端→后端加密、密钥交换
混合加密(RSA/AES、ECDH/AES)公钥公开即可快无有前后端传输加密的标准形态
国密(SM2/SM4/SM3)同对称/非对称中同对标算法无,需第三方库强合规行业指定

回到「前后端传 API Key」这个具体问题:传输的主流落点是混合加密,Key 很短时也可直接用 RSA;存储的主流落点是信封加密。下面按传输层、存储层分别展开。

传输层:HTTPS 是基线,不是全部#

HTTPS(TLS)解决的是链路问题:请求在浏览器和服务器之间的网络传输,不会被窃听、不会被篡改、不会被中间人冒充。这是任何传输敏感数据的产品的强制基线,不是可选配置。

但 TLS 有一个明确边界:它通常在反向代理、CDN 或 WAF 处终止,也就是证书校验和解密发生在网关这一层,而不是你的业务进程。TLS 终止之后,请求体在云厂商网关和后端之间的明文域里流转,网关日志、WAF 日志都可能完整记录下请求内容。对普通请求这无所谓,对 API Key 就是灾难。

所以「前后端传 Key 的加密」,通常不只是指 TLS。它指的是在应用层再加一层加密:让 Key 在浏览器到后端进程之间的任何一层都不可读,只有最终持有私钥的后端进程能解开。这层就叫应用层加解密。

应用层:混合加密,让中间层读不到#

为什么标准形态是混合加密#

上一节那张表里能看到非对称加密的两个约束:明文长度有上限、运算慢。所以即使单条 API Key 通常塞得下 190 字节,主流做法仍倾向混合加密:RSA 只负责护送一个随机生成的对称密钥,真正加密数据的活交给对称加密——速度快、无长度限制、GCM 还自带防篡改。

全局流程#

一次请求的完整路径,前后端各负责一半:

前端(浏览器)                            后端
    │ ① 取 RSA 公钥(/public-key 接口)        │
    ├───────────────────────────────────────▶ │
    │ ② 生成随机 AES-256-GCM 会话密钥           │
    │ ③ AES 加密 API Key → 密文 + IV           │
    │ ④ RSA 公钥加密 AES 密钥 → wrappedKey      │
    │ ⑤ 发 { wrappedKey, iv, ciphertext }      │
    ├───────────────────────────────────────▶ │
    │                             ⑥ RSA 私钥解出 AES 密钥
    │                             ⑦ AES 解密出 API Key
text

这里每一步解决一个问题:

  1. 取公钥:前端需要后端的公钥才能加密。公钥由后端私有,可以安全公开,专门设一个接口下发。
  2. 生成会话密钥:每次请求都生成一个全新的 AES 密钥,用过即弃。这一次的密钥泄露只影响这一次请求,这就是前向保密。
  3. AES 加密数据:API Key 是真正的 payload,交给对称加密处理,速度快、无长度限制。GCM 模式自带认证标签,防篡改。
  4. RSA 包 AES 密钥:RSA 只加密那 32 字节的 AES 密钥,绕开长度限制。只有持有私钥的后端能解开这层。
  5. 前端组装信封:密文、IV、被包裹的 AES 密钥一起发出。三者分开看都是乱码,单独泄露任何一块都解不出 Key。
  6. 后端解包:先用 RSA 私钥解出 AES 密钥,再用 AES 解密。私钥永远不离开后端进程。

为什么浏览器能实现这层#

浏览器原生提供了 Web Crypto API(crypto.subtle),它支持 RSA-OAEP、AES-GCM 等算法,且只在 HTTPS(以及 localhost)这样的安全上下文里可用。这是「在浏览器里做非对称加解密」从偏门库走向标准做法的原因——不依赖任何第三方加密库,纯浏览器能力。

前端加密的示意代码(只依赖 crypto.subtle):

async function encryptApiKey(apiKey, publicKeySpkiB64) {
  // ① 导入后端的 RSA 公钥(SPKI 格式)
  const publicKey = await crypto.subtle.importKey(
    'spki', base64ToBuffer(publicKeySpkiB64),
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false, ['encrypt'],
  );

  // ② 每次请求生成随机的 AES-256-GCM 会话密钥
  const aesKey = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 }, true, ['encrypt'],
  );
  const iv = crypto.getRandomValues(new Uint8Array(12)); // 96 位随机 nonce

  // ③ AES 加密真正的 API Key
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, aesKey,
    new TextEncoder().encode(apiKey),
  );

  // ④ 导出 AES 密钥原始字节,用 RSA 公钥包裹
  const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
  const wrappedKey = await crypto.subtle.encrypt(
    { name: 'RSA-OAEP' }, publicKey, rawAesKey,
  );

  // ⑤ 返回信封,后端只需要这三样
  return {
    keyId: 'k1',
    iv: bufferToBase64(iv),
    wrappedKey: bufferToBase64(wrappedKey),
    ciphertext: bufferToBase64(ciphertext),
  };
}
js

后端解密(Node.js,私钥从配置中心或 KMS 读取,不出进程):

function decryptEnvelope({ wrappedKey, iv, ciphertext }) {
  // ⑥ 用 RSA 私钥解出 AES 密钥
  const aesKey = crypto.privateDecrypt(
    {
      key: process.env.RSA_PRIVATE_KEY,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256',
    },
    Buffer.from(wrappedKey, 'base64'),
  );

  // ⑦ AES-GCM:认证标签是密文最后 16 字节,拆出来单独验证
  const buf = Buffer.from(ciphertext, 'base64');
  const authTag = buf.subarray(buf.length - 16);
  const data = buf.subarray(0, buf.length - 16);

  const decipher = crypto.createDecipheriv(
    'aes-256-gcm', aesKey, Buffer.from(iv, 'base64'),
  );
  decipher.setAuthTag(authTag);
  return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf-8');
}
js

这层的边界#

混合加密防的是「浏览器到后端进程之间」的所有中间层:网络监控、CDN/WAF/反向代理、TLS 终止后的明文域、网关日志。密文落到这些层手里,没有后端私钥都读不出 Key。

它防不了三件事:

  • XSS:Key 在浏览器内存里就是明文,页面脚本可以读走。加密只发生在网络层,救不了被攻陷的页面。
  • 用户本人:Key 是用户自己输入的,他本来就知道。加解密不是防用户的。
  • 后端内部的泄露:后端进程拿到明文 Key 之后,一行错误日志仍然会把它打出去。应用层加密挡不住自己人。

另外有一个隐含前提:公钥本身是从网络上下发的,它依赖 HTTPS 的证书链来保证「拿到的是真后端」。如果公钥被替换成攻击者的,前端加密就等于替攻击者加密。对公钥做固定(把 SPKI 的哈希写进前端,校验下发内容)是纵深防御的做法,多数实现会加。

存储层:KMS 信封加密#

传输加密解决的是「路上被偷」,存储加密解决的是「家里被偷」。API Key 到了后端之后,明文落库等于把所有用户的密钥集中放在一个地方,数据库一旦被拖,损失是全体用户级别的。存储加密和传输加密经常被一起提,但它们是两件独立的事。

落库加密的标准做法还是信封加密,但这次的两层密钥都在后端这一侧:

术语作用
数据加密密钥(DEK)真正加密 API Key 的对称密钥,临时生成
密钥加密密钥(KEK)加密 DEK 的密钥,由 KMS 持有,永不出 KMS

KMS(Key Management Service,密钥管理服务)是云厂商提供的托管密钥服务,职责是保管密钥并只提供加密解密接口。国内云厂商基本都有:阿里云 KMS、腾讯云 KMS、华为云 KMS,自托管方案有 HashiCorp Vault。

完整流程:

  1. 应用调 KMS 的 GenerateDataKey,一次拿回两个东西:明文 DEK 和密文 DEK。
  2. 用明文 DEK 加密 API Key,之后明文 DEK 立即从内存丢弃。
  3. 把密文 DEK 和密文一起存进数据库。
  4. 解密时,应用把密文 DEK 发给 KMS,KMS 用 KEK 解出明文 DEK 返回,应用再用它解密数据。

为什么要绕这一圈,而不是让 KMS 直接加密每一条 API Key?两个原因。一是成本与延迟:每加密一条数据都调一次 KMS 接口,就是一次网络往返加一次计费,用户量上来之后账单和延迟都不可接受;信封加密把 KMS 调用压到「每次存储、每次读取各一次」。二是轮换:KMS 的 master key(KEK)轮换时,只需要换掉 KEK,历史数据不需要重新加密,因为密文 DEK 一直留在数据旁边,用新 KEK 解旧 DEK 照常工作。

存储加密防的是「数据库、备份泄露之后,Key 无法直接读」。它不防「攻击者拿到了应用进程权限」——应用进程本来就有解密能力,进程被拿下,钥匙和锁都在人家手里。

架构层:Key 不要流向前端#

前面几层都是加密手段,这一层是设计决策,也是这整件事里最重要的判断。

把存储的 API Key 发回给前端(后端 → 前端方向)是反模式。加密在这个方向上的问题是结构性的:一旦 Key 回到浏览器,任何加解密都救不了。浏览器要用这个 Key,就必须在浏览器里或 JS 里持有它的明文或解密密钥,而浏览器里的一切都可以被提取。

所以 BYOK 场景的第一条设计原则是:Key 只允许单向流动——用户 → 后端。用户填一次,后端加密存储,之后所有模型调用都由后端代理完成,前端只持有它自己的会话身份(JWT 或会话 cookie),永远不接触第三方 API Key。

在这个原则下,有几种落地的架构,区别在于信任模型和 Key 的存放位置:

架构Key 在哪泄露半径适用场景
后端代理(平台持有)服务端加密存储,调用时后端注入平台被拖库时风险集中多数 BYOK 产品,如 Raycast
短时受限令牌(credential broker)长期密钥留服务端,按任务铸造几分钟 TTL 的令牌令牌只覆盖单次任务、自动过期平台自身凭据、Agent 调用第三方服务
浏览器扩展钱包隔离的扩展进程或本地加密 vault,网页拿不到不进平台,泄露仅限本机用户自己管理 Key,不经过平台

后端代理是最常见的选择。用户填一次 Key,平台加密存储,每次调用由后端替用户带 Key 请求供应商。前端的请求里只有用户自己的会话身份,第三方 API Key 在浏览器里不存在,也就无所谓「传」。

短时受限令牌是另一种思路,参考 OAuth 2.0 Token Exchange(RFC 8693)和各家 ephemeral token 的做法:长期密钥留在服务端,一个代理按任务铸造短命、限定范围的令牌,客户端用令牌做事,令牌几分钟后过期,吊销即刻生效。它比长期 Key 的泄露半径小得多。但对 BYOK 场景有一个现实限制:OpenAI、Anthropic 这类第三方供应商不会提供一个「平台可以替用户铸造的短时 Key」,短时令牌只能用于平台自身的凭据,替代不了用户那枚长期 API Key。所以对第三方模型 Key,后端代理仍是主力,短时令牌管的是平台自己这一侧的凭据。

浏览器扩展钱包是信任模型最极端的一种:Key 存在浏览器扩展的隔离进程或本地加密 vault 里(常见实现是 AES-256-GCM 加密、用 PBKDF2 从主密码派生密钥),网页应用只拿到短时会话令牌,Key 从不离开用户设备。代价是每个使用方都要做一次扩展安装和授权,适合个人工具,不适合多租户的云产品。

运维层:脱敏、验证、轮换#

加密解决不了所有泄露,运维纪律补上剩下的缺口。

日志。 绝不打印完整 Key。调试需要时只记末 4 位(sk-...abcd),请求头在网关和后端的日志里都要脱敏,错误上报平台要过滤请求体。应用层加密再强,一行 console.log(req.body) 就前功尽弃。

入库前验证。 用户提交 Key 时,先做一次最小成本的 live test(真实调用一次验证 Key 有效),避免把无效或写错的 Key 存进库。验证请求本身也要走加密传输。

浏览器防线。 前面说过 XSS 能读走内存里的 Key。CSP(Content Security Policy,内容安全策略)限制页面能加载和执行什么,是浏览器端压缩 XSS 影响面的手段之一,和加密配合使用。

轮换与吊销。 这比任何加密都前置。一个 Key 一旦疑似泄露,能立刻吊销才有意义,否则加密只保护了「坏人偷到手之前的时刻」。加解密不替代吊销机制,两者都要有。

选型与结论#

把四层方案按「防什么、Key 在哪、边界」拉成一张表:

方案防的威胁主要边界
HTTPS / TLS网络窃听、链路中间人在网关终止,不防中间层与端点
应用层混合加密(RSA-OAEP + AES-GCM)浏览器到后端进程之间的所有中间层不防 XSS、不防端点
KMS 信封加密数据库与备份泄露后直接读 Key不防应用进程被拿下
架构层代理 / 短时令牌从根上缩小 Key 暴露面依赖架构决策,换掉成本高

加解密按威胁模型分层成几件事:传输、存储、架构、运维,各管一段。对 MaaS/BYOK 场景,决策顺序通常是:先保证 Key 单向流动(架构层),再加密落库(存储层),按需要加应用层传输加密(传输层),最后把日志、验证、轮换的纪律立起来(运维层)。

传输加密是纵深防御的必要零件。真正影响一次泄露严重程度的,是 Key 曾经出现在多少个不该出现的地方;把 Key 的数量和流动方向管住,比把它加密得更复杂更能降低风险。

API Key 传输加解密:从混合加密到密钥流动
https://blog.r3actgo.com/blog/2026/tech/api-key-encryption-transmission
Author r3actgo
Published at August 3, 2026