</>开发学习笔记
← 返回全部笔记
安全

密码哈希要限并发:一次 128 MB 的计算

2026-11-08阅读约 2 分钟

密码用 scrypt 存储,参数取了常见的推荐值:N = 2^17, r = 8, p = 1。这组参数下算一次大约 100 毫秒,看起来不多,但有两件事容易被忽略。

一次要 128 MB 内存

scrypt 的内存占用约为 128 × N × r 字节,这组参数正好是 128 MB。Node.js 默认的上限只有 32 MB,所以必须显式传 maxmem,否则直接抛错。这个错只在真实参数下出现,用小参数写的测试发现不了,所以测试里专门留了一条用生产参数真跑的用例。

更大的问题是并发:四个人同时登录,就是 512 MB。对一台 4 GB 内存、还跑着数据库的小服务器来说,几个人反复刷登录接口就能把内存挤满。

还占着线程池

异步的 scrypt 跑在 libuv 的线程池里,默认只有 4 个线程,而文件读写、DNS 解析也用这个线程池。四个密码同时在算的时候,别的请求读个文件都得排队。

做法:限流,并且「忙」不算「密码错」

加了一个很小的限流器:同时最多算 2 个,排队最多 20 个,再多就直接返回 503 和「稍后再试」。

async run(fn) {
  if (this.active >= this.max) {
    if (this.waiting.length >= this.maxQueue) throw new BusyError();
    await new Promise(r => this.waiting.push(r));
  } else {
    this.active++;
  }
  try { return await fn(); }
  finally {
    const next = this.waiting.shift();
    if (next) next(); else this.active--;   // 名额直接交给下一个
  }
}

有一个细节差点写错:原来的校验函数为了防脏数据,把所有异常都吞掉、返回「密码不对」。如果把限流放进这个 try 里面,「服务器忙」就会被当成一次密码错误,计入错误次数,用户莫名其妙被要求等待。所以排队和拒绝必须放在 try 外面。

验证方法和前一篇一样:写完测试先故意改坏实现。把「忙」也吞成 false 之后,确实有两条测试变红,说明这条边界是有测试守着的。