










书接上回,选定 Typecho-CF 之后,我兴冲冲地把它打包部署到了 Cloudflare Workers 上,本以为马上就能用上,没想到出师不利,初始化这一步就被卡住了。
代码发布上去后,我打开绑定的域名进入 /install 页面,输入站点名称、管理员账号和密码,点击“开始安装”。浏览器转圈两三秒,随后弹出了一个红色错误页:
Error 1102: Worker exceeded CPU time limit.
博客没能初始化成功,数据库里的初始表和管理员账号也只写了一半。
我把当时的 Worker 运行日志和安装代码发给 AI 协助排查。AI 顺着调用栈逐行分析,很快指出了问题所在:在创建管理员账号时,密码哈希计算耗尽了 CPU 时间。
Cloudflare Workers 对请求有严格的算力限制:
这里的 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 边缘环境下,每个请求都受到严格的资源配额约束,必须想办法节省性能开销。
当时摆在我面前的有两条路:
我折腾这套方案的初衷就是验证免费配额下运行完整博客的可行性,直接花钱升级违背了最初的目标。
但如果只是简单地把迭代次数砍到 5 万次,密码的抗碰撞强度就会明显下降。万一后续数据库备份不慎泄漏,攻击者在本地使用显卡跑暴力破解的成本会大大降低。
为了在 10ms 配额内兼顾运算速度与密码安全,我采用了 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)
PASSWORD_PEPPER 没有泄露,攻击者在本地拿到 hash 和 salt 也无法直接构建字典进行离线暴力破解。在 src/lib/auth.ts 中,我通过哈希前缀来区分是否启用了 Pepper:
$PBKDF2$<iterations>$<salt>$<hash>$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) 进行检测:
$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,顺利完成了初始化。
解决了初始化的算力限制后,接下来就是把旧博客里的文章、独立页面、短篇随笔和评论数据迁移过来。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。