perf(用户导入): 批量导入从几秒降到半秒
Deploy / deploy (push) Has been cancelled

导入一个班(45 人)要转好几秒,慢的全在密码哈希这一处:

- 哈希在重名检查之前算。老师习惯把同一份名单粘两次,那种情况要白等
  一整个班的 argon2 才看到 409。把校验和重名查询提到前面,这条路径
  现在是 5.6ms 返回。
- 45 次 argon2 串行 await。改成固定 4 路并发池;不用 Promise.all 是因为
  oj-api 的 mem_limit 只有 512m,一个年级 300 人全量并发撑不住。
- argon2id 参数从 Bun 默认的 m=64MiB 显式降到 OWASP 推荐下限 m=19MiB,
  单次 140ms → 20ms。参数编码在哈希串里,存量账号照常验证、不用迁移,
  旧的 pbkdf2 那条分支也不受影响。

45 人端到端 2.04s → 0.55s。验过:旧 m=65536 的哈希、新 m=19456 的哈希、
Django 的 pbkdf2 三种都能正常登录,错误密码照常拒绝。

顺带把生成页下载的 CSV 裁成用户名和密码两列 —— 发给学生的就这两样。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
This commit is contained in:
2026-09-01 00:28:16 -06:00
co-authored by Claude Opus 5
parent e5a6f2d1e7
commit a2f67eebf6
3 changed files with 39 additions and 11 deletions
+17 -1
View File
@@ -53,6 +53,22 @@ export async function verifyPassword(password: string, encoded: string) {
return { valid: false, needsUpgrade: false }
}
/**
* 参数是显式写死的,不用 Bun 的默认值(`m=65536, t=2, p=1`,即每次哈希占 64 MiB)。
* 取的是 OWASP 对 argon2id 的推荐下限 `m=19MiB, t=2, p=1`
*
* - 批量导入一个班要连算几十次哈希,64 MiB 那档单次 ~140ms,而 `oj-api` 的
* mem_limit 只有 512mdocker/compose.debian.yml),并发度被内存卡死。
* 19 MiB 这档单次 ~20ms,并发 4 路的峰值也才 76 MiB。
* - 参数是编码进哈希串本身的(`$argon2id$v=19$m=19456,t=2,p=1$...`),所以**存量
* 账号一个都不用迁移**Bun.password.verify 读串里的参数验,改这里只影响此后新写的哈希。
*/
const ARGON2_OPTIONS = {
algorithm: "argon2id",
memoryCost: 19456,
timeCost: 2,
} as const
/**
* 写密码的**唯一入口**。五个调用方都走这里:注册、管理员改密码、批量导入用户、
* 重置密码、登录时升级存量 pbkdf2。
@@ -68,5 +84,5 @@ export async function verifyPassword(password: string, encoded: string) {
* 账号;靠开关只能拦住将来,修不了已经发生的。
*/
export function hashPassword(password: string) {
return Bun.password.hash(password, { algorithm: "argon2id" })
return Bun.password.hash(password, ARGON2_OPTIONS)
}