惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
Y
Y Combinator Blog
Jina AI
Jina AI
G
Google Developers Blog
Last Week in AI
Last Week in AI
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
The GitHub Blog
The GitHub Blog
D
Docker
量子位
罗磊的独立博客
腾讯CDC

设计笔记

前台体验重塑 —— 从笔记动态到评论动静分离 从多级缓存到 Workers Caching,资源开销骤降 98.7% 性能优化实践 从 WordPress 到 Typecho-Workers 的数据映射与导入 将 Typecho 跑在 Cloudflare Workers 1Panel V1 升级 V2,以及命令注入漏洞 CVE-2025-54424 相关问题 白嫖Cloudflare R2,失误反被扣费 WordPress社交营销优化之OpenGraph Image()函数加载base64图片无onload事件的前端兼容方案 沪飘一年 写给2020 Adams主题问题收集与更新计划 攒了半年的水事 写作乐趣不再 小米路由器青春版刷华硕固件
10ms 魔咒 —— PBKDF2 降档与 HMAC Pepper 安全加固
T Tokinx 静水流深,美语善缘 · 2026-08-23 · via 设计笔记

PortingTypecho2Workers

书接上回,选定 Typecho-CF 之后,我兴冲冲地把它打包部署到了 Cloudflare Workers 上,本以为马上就能用上,没想到出师不利,初始化这一步就被卡住了。

代码发布上去后,我打开绑定的域名进入 /install 页面,输入站点名称、管理员账号和密码,点击“开始安装”。浏览器转圈两三秒,随后弹出了一个红色错误页:

Error 1102: Worker exceeded CPU time limit.

博客没能初始化成功,数据库里的初始表和管理员账号也只写了一半。


为什么会 CPU 超时?

我把当时的 Worker 运行日志和安装代码发给 AI 协助排查。AI 顺着调用栈逐行分析,很快指出了问题所在:在创建管理员账号时,密码哈希计算耗尽了 CPU 时间。

Cloudflare Workers 对请求有严格的算力限制:

  • Free 计划:单个请求的纯 CPU 时间上限为 10ms
  • Paid 计划:默认 50ms,最高可调整至 3000ms。

这里的 10ms 指的是 CPU 实际计算时间,等待数据库读写或网络 IO 的时间并不计入其中。

原项目为了遵循 OWASP 2024 关于密码存储的安全标准,默认采用了 600,000 次 PBKDF2-HMAC-SHA256 迭代:

export const PBKDF2_ITERATIONS = 600_000;

在本地开发机或独立服务器上,跑 60 万次哈希大概需要 30 到 50ms。因为用户注册和登录并不频繁,这点开销在传统服务器上完全可以忽略。

但在 Workers Free 计划的单核沙箱环境里,CPU 算力相对有限,这 60 万次哈希计算耗时在 40ms 到 60ms 之间。执行到 10ms 阈值时,Worker 就会被运行时直接终止,抛出 1102 错误。


两难的选择

在传统的服务器开发环境里,60 万次迭代甚至更高的计算成本是非常普遍的做法,开发者通常不需要为几毫秒的 CPU 开销而顾虑。但在 Serverless 边缘环境下,每个请求都受到严格的资源配额约束,必须想办法节省性能开销。

当时摆在我面前的有两条路:

  1. 升级到付费版:购买每月 $5 的 Workers Paid 计划,CPU 上限放宽到 50ms,问题当场解决;
  2. 调低迭代次数:把 PBKDF2 迭代压到 50,000 次以内,让计算耗时控制在 8ms 左右。

我折腾这套方案的初衷就是验证免费配额下运行完整博客的可行性,直接花钱升级违背了最初的目标。

但如果只是简单地把迭代次数砍到 5 万次,密码的抗碰撞强度就会明显下降。万一后续数据库备份不慎泄漏,攻击者在本地使用显卡跑暴力破解的成本会大大降低。


解决方案:50k 迭代 + HMAC Pepper

为了在 10ms 配额内兼顾运算速度与密码安全,我采用了 Pepper(密码胡椒)方案。

什么是 Pepper?

常见的 Salt(盐)是随机生成的,明文存放在数据库的用户表里,主要用来防范彩虹表。

Pepper 则是一个独立于数据库的高熵密钥,保存在 Cloudflare Workers 的安全环境变量 PASSWORD_PEPPER 中。

计算密码哈希时,先用 PASSWORD_PEPPER 对原始明文密码做一次 HMAC-SHA256 计算,再把输出结果送入 50,000 次 PBKDF2 进行迭代加盐计算。

[明文密码] → [HMAC-SHA256] (Pepper 密钥) → [PBKDF2-SHA256] (50k 迭代 + 16B Salt) → [$PBKDF2P$50000$salt$hash] (写入 D1)

兼顾速度与安全的原因

  1. 计算耗时低:一次 HMAC-SHA256 计算耗时不到 0.05ms,加上 5 万次 PBKDF2 迭代,总 CPU 时间稳定在 6 到 8ms,能完全装进 10ms 配额内。
  2. 防脱机碰撞:即便 D1 数据库被全量导出,只要保存在 Cloudflare 环境变量里的 PASSWORD_PEPPER 没有泄露,攻击者在本地拿到 hash 和 salt 也无法直接构建字典进行离线暴力破解。

代码实现与平滑升级

src/lib/auth.ts 中,我通过哈希前缀来区分是否启用了 Pepper:

  • 无 Pepper 格式:$PBKDF2$<iterations>$<salt>$<hash>
  • 有 Pepper 格式:$PBKDF2P$<iterations>$<salt>$<hash>
export async function hashPassword(password: string): Promise<string> {
  const iterations = getPbkdf2Iterations(); // 默认 600,000,Free 模式读取配置 50,000
  const pepper = env.PASSWORD_PEPPER?.trim();
  const salt = crypto.getRandomValues(new Uint8Array(16));
  
  // 1. 若配置了 Pepper,先执行 HMAC-SHA256 预哈希
  const payload = pepper
    ? await hmacSha256(pepper, password)
    : new TextEncoder().encode(password);

  // 2. 执行 PBKDF2 派生密钥
  const derived = await pbkdf2Sha256(payload, salt, iterations, 32);
  const saltHex = bufferToHex(salt);
  const hashHex = bufferToHex(derived);

  // 3. 带 P 前缀标记
  const prefix = pepper ? '$PBKDF2P$' : '$PBKDF2$';
  return `${prefix}${iterations}$${saltHex}$${hashHex}`;
}

登录时自动升级哈希

当用户登录成功时,程序会调用 passwordHashNeedsRehash(user.password) 进行检测:

  • 如果当前配置了 Pepper,但数据库里的记录还是旧版的 $PBKDF2$
  • 或者数据库里的迭代次数低于当前配置的 PBKDF2_ITERATIONS

系统会在登录成功后用新标准重新生成哈希并更新到数据库中,不需要手动重置密码。

if (passwordHashNeedsRehash(user.password)) {
  const upgradedHash = await hashPassword(password);
  await db.update(schema.users)
    .set({ password: upgradedHash })
    .where(eq(schema.users.uid, user.uid));
}

配置方式

如果你同样运行在 Cloudflare Workers Free 计划上,可以在 wrangler.toml 中配置环境变量:

[vars]
PBKDF2_ITERATIONS = "50000"

同时在 Workers 后台添加 Pepper 密钥:

wrangler secret put PASSWORD_PEPPER

结果

调整之后,再次执行 /install 安装流程,整个接口耗时从超时失败降至约 7ms,顺利完成了初始化。

解决了初始化的算力限制后,接下来就是把旧博客里的文章、独立页面、短篇随笔和评论数据迁移过来。