必须显式设置 bcrypt.generatefrompassword 的 cost 参数(推荐12)、校验空密码、确保数据库字段长度≥60且无前后空格及$2y$前缀——否则 comparehashandpassword 会因哈希格式非法报 invalid hash。

直接用 bcrypt.GenerateFromPassword 不设 cost 参数、不校验空密码、不检查数据库字段长度,就上线——90% 的 “invalid hash” 和 “hashedPassword is not the hash of the given password” 错误都源于这三点。
为什么 CompareHashAndPassword 总报 invalid hash
它不是在说“密码错了”,而是在拒绝解析你传进去的字符串。真正出问题的是哈希本身,不是明文。
-
len(hashedPassword)不是 60:MySQL 字段定义为VARCHAR(50)、GORM 自动截断、JSON 序列化时 trim 空格,都会导致哈希被砍掉几字节 - 哈希开头是
$2y$:Go 官方golang.org/x/crypto/bcrypt不支持 PHP 老系统遗留的$2y$前缀,必须手动转成$2b$ -
user.Password是空字符串或 nil:查用户失败后没判 err,直接拿零值结构体去比对,CompareHashAndPassword遇到空输入会 panic - 前后有空格:前端没 trim、日志里打出来看着像对,但实际比对时
"$2a$12$..."变成了" $2a$12$..."
GenerateFromPassword 的 cost 参数不能省
默认 bcrypt.DefaultCost 是 10,2026 年已不够用:太低易被暴力破解,太高会让登录接口 P95 延迟飙到 300ms+,尤其在容器化环境里容易触发连接积压甚至 too many open files。
- 新项目统一用
cost = 12:实测主流云服务器单次耗时约 80–120ms,QPS 可稳住 50+ - 从配置读取后必须校验范围:
if cost 31,否则GenerateFromPassword直接 panic 报invalid cost - 永远传
[]byte(password),别提前strings.TrimSpace——空格是密码的一部分 - 注册前强制非空校验:
if len(password) == 0或len(strings.TrimSpace(password)) ,避免静默哈希空密码
数据库字段和查询逻辑怎么配才不出错
哈希不是普通字符串,字段设计和 ORM 使用方式稍有偏差,验证就必挂。
- 存哈希的字段类型必须是
VARCHAR(60)或TEXT(PostgreSQL 推荐后者),禁用CHAR和带隐式截断的短VARCHAR - GORM 中确保
User.Password字段加了gorm:"column:password",且没被json:"-"或gorm:"-"忽略写入 - 登录查询必须先检查错误:
err := db.Where("email = ?", email).First(&user).Error,err != nil就直接返回,绝不进比对流程 - 比对前加空值防护:
if len(user.Password) == 0,避免把空字符串喂给CompareHashAndPassword
迁移老数据或切换算法时最易忽略的点
安全升级不是改一行代码的事,而是整条链路的协同。
- 遇到
$2y$哈希,别硬改 bcrypt 源码,用正则提取^\$2y\$(\d+)\$([A-Za-z0-9+/]{22}),拼成$2b$${cost}$${salt}再验 - 升级 cost 值时,旧哈希仍要能验:用
bcrypt.Cost([]byte(oldHash))提取原 cost,匹配成功才走旧路径 - 切到 Argon2 或 scrypt?别直接替换——参数需单独存、验证逻辑完全不同,必须双写过渡 + 全量重哈希
- 所有日志、调试输出中,禁止打印明文密码(哪怕只是
fmt.Printf("%s", pwd)),中文、emoji 会加剧编码混乱风险
最常被跳过的其实是入库前的哈希长度校验和查询后的空值判断——它们不炫技,但只要漏一个,整个密码流程就不可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











