不能直接替换bcrypt哈希字符串,因scrypt/argon2参数不内嵌于哈希中,需单独存储并重写验证逻辑;升级必须通过“双写+渐进式重哈希”,在用户登录时用明文同步生成新旧两套哈希,且旧字段不可删、新哈希须带算法标识,避免并发覆盖与逻辑失控。

为什么不能直接替换 bcrypt 哈希字符串
bcrypt 哈希字符串(如 $2a$10$...)自带版本、cost、salt 和 hash 全部信息,验证时无需额外字段。但 scrypt 或 Argon2 的参数(如 N, r, p, salt)不内嵌在哈希输出中,必须单独存库、手动传入验证函数。这意味着:你不能只改一个 bcrypt.CompareHashAndPassword 调用就切到 Argon2——验证逻辑、存储结构、迁移路径全得重写。
升级必须走「双写 + 渐进式重哈希」流程
用户下次登录时才是唯一安全的升级时机。此时你已有明文密码,可同时做两件事:用新算法生成哈希并存入新字段(如 password_hash_v2),并保留旧哈希用于本次比对。关键点:
- 旧哈希字段(如
password_hash)绝不能删,直到 100% 用户都触发过登录 - 新哈希字段需带算法标识(如
$argon2id$v=19$m=65536,t=3,p=4$...),否则未来再升级会更难 - 数据库要支持同一用户存在两种哈希字段,且读取逻辑能自动识别当前该用哪个
- 不要在注册/修改密码时批量重哈希——那会阻塞请求、拖慢响应,还可能因并发写导致数据错乱
如何判断哪些用户还在用旧 bcrypt 哈希
bcrypt.Cost 函数能从现有哈希字符串里解析出 cost 值。比如你发现大量用户哈希的 cost 是 4 或 6,说明当年部署时设得太低,现在该优先推动这批人登录升级:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
hash := []byte("$2a$04$abc...") // 示例旧哈希
cost, err := bcrypt.Cost(hash)
if err != nil {
// 不是合法 bcrypt 哈希,可能是其他算法或损坏数据
}
// cost == 4 表示计算强度严重不足,建议强制要求下次登录升级
注意:bcrypt.Cost 只对 $2a$ / $2b$ / $2y$ 开头的字符串有效;对 $argon2id$ 或 $scrypt$ 直接 panic。
别让 wrapper 库偷偷帮你“自动升级”
有些第三方封装库声称支持“透明算法迁移”,内部会在 Compare 成功后自动用新算法重哈希并覆盖旧值。这很危险:
- 它把写库操作藏在验证逻辑里,违反单一职责,调试时极难定位谁改了密码字段
- 高并发登录下多个 goroutine 可能同时读到旧哈希、同时计算新哈希、同时写回,造成最终只有一条写入生效,其余丢失
- 你无法控制重哈希时机——比如想等低峰期批量处理,或想加监控埋点,这类库根本不留钩子
真正可控的方式,是把重哈希逻辑显式放在业务层:登录成功 → 校验通过 → 检查是否需升级 → 调用 argon2.IDKey 生成新哈希 → 显式更新数据库 → 记日志。每一步都可测、可观、可回滚。










