应严格控制明文≤72字节、cost=12、数据库字段≥255字符,三者缺一即导致invalid hash错误;明文超长被静默截断,cost非12易致爆破或超时,字段不足60字节则哈希损坏。

bcrypt.GenerateFromPassword 和 bcrypt.CompareHashAndPassword 本身不提供“高强度”开关,所谓高强度,是靠你控制三个硬性条件:明文长度 ≤ 72 字节、cost 设为 12、数据库字段能完整存下 60 字节哈希。漏掉任意一条,校验就大概率报 invalid hash 或 hashedPassword is not the hash of the given password。
明文密码必须 ≤ 72 字节,且不能为空
Go 的 bcrypt.GenerateFromPassword 对输入的 []byte 静默截断——超过 72 字节的部分直接丢弃,但不报错。用户输“>Password123…(含 emoji)”和“>Password123…(前 72 字节)”,哈希结果完全一样。
- 注册入口必须校验:
if len([]byte(pwd)) == 0 || len([]byte(pwd)) > 72,返回明确错误(如"password length must be 1–72 bytes") - 别依赖前端限制,服务端必须重校;中文、emoji、全角字符都按 UTF-8 字节数算
-
strings.TrimSpace不够用,要先strings.Map过滤不可见控制符(如\u200b),再判空和长度
cost 参数必须显式设为 12,不能省略或硬写 10/4
bcrypt.DefaultCost 当前是 12,但它只是个常量,不是运行时自动适配值。硬写 10 安全性已不足;设成 4(测试常用)上线后会因延迟骤增导致连接积压甚至 too many open files。
- 永远显式传
bcrypt.DefaultCost,而不是留空或写死数字 - 从配置读取
cost后必须校验范围:if cost 31,否则GenerateFromPassword直接 panic - 容器化环境里,
cost = 12实测单次耗时 80–120ms,QPS 可稳住 50+;设成 14 就可能突破 1s,登录接口易超时
数据库字段和比对逻辑必须严格匹配哈希格式
bcrypt.CompareHashAndPassword 不做容错解析,它只认标准格式:以 a$ 或 b$ 开头、长度恰好 60、校验和有效。任何偏差都会立刻拒绝,报 invalid hash。
- MySQL 字段至少定义为
VARCHAR(255),VARCHAR(50)必截断;PostgreSQL 推荐用TEXT - 存库前必须转
string(hashed),绝不能存[]byte二进制——读出来会乱码或 decode 失败 - 查出哈希后,立即
strings.TrimSpace再传给CompareHashAndPassword;日志里看着像对,实际可能开头多了空格 - 参数顺序不能反:
CompareHashAndPassword([]byte(storedHash), []byte(plainPwd)),颠倒必报错
迁移老数据或兼容 PHP 旧系统时,前缀转换不能跳过
Go 官方 golang.org/x/crypto/bcrypt 不支持 $2y$ 前缀(PHP 早期实现遗留),遇到就 panic。但哈希主体(salt + cipher)是兼容的,只需替换前缀。
立即学习“go语言免费学习笔记(深入)”;
- 上线前加一层校验:
regexp.MustCompile(`^\$2[ab]\$\d{2}\$[./A-Za-z0-9]{53}$`).MatchString(hash) - 迁移脚本中,对
$2y$10$...类哈希,用正则提取 cost 和 salt 段,拼成$2b$10$...再验 - 新生成全部用
$2b$(Go 默认输出),更严格处理 null 字节,避免潜在解析歧义
VARCHAR(50) 截断、日志打印时换行符混入。调试第一件事不是改逻辑,而是打 len(user.Password),不是 60 就立刻回溯存储路径。


















