必须在浏览器中携带的 API 密钥

限制它、代理它、轮换它,然后再混淆剩下的部分。在打包产物中编码一个凭证究竟能带来什么,界限又在哪里。

有时密钥或令牌必须出现在客户端打包产物中,因为浏览器要直接发起调用。混淆只改变了这个问题的一部分,而不是全部。

可读的密钥会被机器人发现,而不是人

以可读文本形式出现在已发布 JavaScript 中的密钥,任何人一次 grep 就能找到。爬取型密钥扫描器无需任何人手动打开你的网站就能发现它,而这种自动化发现正是密钥从「已发布」走向「被滥用」的常见路径。

不需要有人对你的产品感兴趣才会发生这件事。机器人会检查它能访问到的每一个公开打包产物,寻找形似凭证的字符串,而你的密钥和其他人的一起排在队列里。

编码密钥能买到什么,界限又在哪里

strings.encode 在任何正值的 complexity 目标下都会开启,因此源代码中的字符串字面量会在输出中被编码,而不是以文本形式发布。以形似凭证的字符串对打包产物做 grep 的扫描器,用这种方式找不到密钥。

这是有价值的,但也仅此而已。混淆隐藏的是一个值如何被使用,而不隐藏这个值本身:浏览器运行你代码时所需要的任何内容,都可以通过运行那段代码来获取。被封装起来的密钥并不是安全的密钥。

限制、代理、轮换,然后再混淆

按顺序做这四件事,并把混淆当作最后一步:

  1. 在提供方那里把密钥限制到它所提供的最窄范围、来源和配额。
  2. 把授权逻辑移到你自己的端点之后,让浏览器永远不持有真正的凭证。
  3. 轮换已经发布过的密钥。从它发布的那一刻起就假定它已经泄露。
  4. 混淆那些确实无法迁移到服务端的部分。

对于最后一步,默认构建已经会对它编码。要为 CI 设置构建门槛:

{
  "strings": {
    "leaks": {
      "max": 0
    }
  }
}

指令可以在 Pro 构建中把某一个值单独提升到高于程序其余部分的保护级别。

const apiKey = /* @afterpack preset=extreme */ "YOUR_SECRET";

一旦出现可读字面量就让构建失败

--strings.leaks.max=0 会在可读字面量存活下来的那一刻让构建失败,并给出 DIAG_SURVIVING_READABLE_LITERAL 的名字。把它接入 CI,这样就不必靠肉眼检查了。要查看一个已部署的网站眼下暴露了什么,对它运行 npx afterpack@latest audit。

已发布的密钥依然是已发布的密钥

编码提高了通过阅读已发布文件来找到密钥的成本,一个通过了泄露门槛的构建是一个真实的结果。但这不代表这个值已经无法触及:它依然存在于输出中,只是被编码了,并会在你的代码使用它的那一刻被还原,因此任何在自己可控的浏览器中运行该页面的人都能把它读回来。轮换密钥并把授权逻辑迁移到你自己的服务器,才是真正消除暴露的步骤。

下一步

  • 常见问题:对这个具体问题的简短回答。
  • 威胁模型:静态读取者与运行你代码的人之间的边界。
  • 最佳实践:如果密钥已经发布出去,该如何轮换。