导入一个班(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:
@@ -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 只有 512m(docker/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)
|
||||
}
|
||||
|
||||
@@ -238,18 +238,15 @@ adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => {
|
||||
return failure(c, 400, "invalid-request", parsed.error.issues[0]?.message ?? "Invalid payload")
|
||||
}
|
||||
const rows = parsed.data.users
|
||||
const prepared: { username: string; password: string; raw: string; email: string; realName: string; className: string | null }[] = []
|
||||
type Prepared = { username: string; password: string; raw: string; email: string; realName: string; className: string | null }
|
||||
|
||||
// 先把不花钱的校验全做完,再动 argon2。班级号错、用户名重复这两种情况占了失败的绝大多数
|
||||
// (老师习惯把同一份名单粘两次),先算哈希的话要白等一整个班的 argon2 才看到报错。
|
||||
const prepared: Prepared[] = []
|
||||
for (const [username, password, email, realName] of rows) {
|
||||
const className = classNameOf(username)
|
||||
if (!className.ok) return failure(c, 400, "invalid-class-name", className.message)
|
||||
prepared.push({
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
raw: password,
|
||||
email,
|
||||
realName,
|
||||
className: className.value,
|
||||
})
|
||||
prepared.push({ username, password: "", raw: password, email, realName, className: className.value })
|
||||
}
|
||||
|
||||
const existing = await db.select({ username: schema.user.username }).from(schema.user)
|
||||
@@ -258,6 +255,19 @@ adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => {
|
||||
return failure(c, 409, "username-exists", `用户名已存在:${existing.map((row) => row.username).join("、")}`)
|
||||
}
|
||||
|
||||
// argon2id 是**故意**做慢的,串行 await 的话一个班要转好几秒。但也不能 Promise.all
|
||||
// 全量:每次哈希占 m=19MiB(见 auth/password.ts 的 ARGON2_OPTIONS),一个年级 300 人
|
||||
// 同时开就是 5.7GB,而 oj-api 的 mem_limit 只有 512m(docker/compose.debian.yml)。
|
||||
// 固定 4 路并发,瞬时峰值 76MiB 封顶。
|
||||
const HASH_CONCURRENCY = 4
|
||||
let cursor = 0
|
||||
await Promise.all(Array.from({ length: Math.min(HASH_CONCURRENCY, prepared.length) }, async () => {
|
||||
while (cursor < prepared.length) {
|
||||
const item = prepared[cursor++]!
|
||||
item.password = await hashPassword(item.raw)
|
||||
}
|
||||
}))
|
||||
|
||||
// 整批要么全进要么全不进 —— 导入是粘一整个班的名单,进了一半再重试会撞已存在
|
||||
const created = await db.transaction(async (tx) => {
|
||||
const users = await tx.insert(schema.user).values(prepared.map((item) => ({
|
||||
|
||||
Reference in New Issue
Block a user