bcrypt 是什么
bcrypt 是基于 Blowfish 密钥编排的自适应口令散列函数,1999 年由 Provos 与 Mazieres 在 USENIX 论文中提出。它自带随机盐值,并用可调的 cost 因子控制计算量:cost 每加 1,迭代轮数翻倍、单次校验耗时也大致翻倍。这样在硬件每年变快的情况下,只要调高 cost 就能抵消显卡与字典爆破带来的收益。输出是固定 60 字符的字符串,前缀标识算法变体,盐值与 cost 都编码在里面,所以数据库只存这一列就够。
基本信息
| 适用平台 | Linux/Unix 及主流语言库(Python bcrypt、PHP password_hash、Go golang.org/x/crypto/bcrypt) |
|---|---|
| 存储标识 | $2b$(部分旧实现为 $2a$/$2y$) |
| cost 取值 | 4-31,默认 10-12,需按实际硬件基准调整 |
| 输入上限 | 72 字节,超出部分被静默截断 |
| 盐值 | 由库生成并编码进结果字符串,不需要单独字段 |
| 标准来源 | USENIX 1999 论文(Provos & Mazieres) |
典型使用场景
- 网站与后台系统存储登录口令:替换 md5/sha1 的裸散列,天然带盐、抗彩虹表
- 生成 htpasswd 给 Nginx/Apache 做基础认证,用 -B 参数指定 bcrypt 算法
- 迁移存量弱散列口令:用户下次登录校验成功后重算为 bcrypt 并回写数据库
- 数据库泄露后的兜底防线:即便拖库,还原明文的成本也远高于一次深度调参的爆破预算
- 与 Argon2id 的取舍:老语言栈或老依赖优先 bcrypt,新项目且依赖齐备时可评估 Argon2id
生成与校验
生成与校验
htpasswd -bnBC 12 user 'password' # -B 用 bcrypt,-C 12 指定 cost,输出 user:$2y$12$...
# Python
import bcrypt
h = bcrypt.hashpw(b'password', bcrypt.gensalt(rounds=12)) # 每次调用盐值不同
yes = bcrypt.checkpw(b'password', h) # 校验用 checkpw,不要自己写字符串比较
# 长口令先做 SHA-256 预处理(这个规则必须固定并写进文档)
import hashlib, base64
digest = hashlib.sha256(pw.encode()).digest()
h2 = bcrypt.hashpw(base64.b64encode(digest), bcrypt.gensalt(rounds=12))参数与行为对照
| 项 | 含义 | 建议与注意 |
|---|---|---|
| cost | 迭代轮数的指数,每加 1 耗时约翻倍 | 取值让单次校验落在 100-300 毫秒 |
| salt | 随机盐,编码进结果字符串 | 交给库生成,不要自己拼 |
| 72 字节上限 | 输入被截断的边界 | 超长口令先做 SHA-256 加 Base64 预处理 |
| $2b$/$2y$ | 同一算法的标识变体 | 跨语言对接前用同一口令做兼容性测试 |
| checkpw | 恒定时间比较 | 必须使用,手写比较会泄露时序信息 |
超过 72 字节的口令会被静默截断:两个前 72 字节相同的长口令会被判为同一口令。长口令场景先做 SHA-256 预处理并在文档中固定该规则,或改用 Argon2id。
cost 的调整不必重置存量散列:校验时可以从存储的字符串里读出当初的 cost,用户下次登录成功后再用新 cost 重算一次,自然完成升级。
bcrypt 与 Argon2id 的差别在于资源维度:Argon2id 同时消耗内存与 CPU,抗显卡与专用电路更强且没有 72 字节上限,但参数更多、老语言栈支持度不如 bcrypt。稳妥做法是先按 bcrypt 上线,成功校验后再迁移为 Argon2id。