
bcrypt密码哈希:开发者实用指南
📷 Pixabay / Pexelsbcrypt密码哈希:开发者实用指南
关于密码安全bcrypt的一切——成本因子、在Node.js/Python/PHP中的实现,以及要避免的常见错误。
如果你曾经需要存储用户密码,你可能被告知要"使用bcrypt"。这是二十多年来的标准建议,有充分的理由。但理解为什么它有效——以及如何正确使用它——才是安全实现与看起来安全的实现之间的区别。
本指南涵盖所有实用内容:bcrypt实际如何工作、选择哪个成本因子、如何在多种语言中实现,以及悄然破坏密码安全的错误。
bcrypt是什么,为什么存在
bcrypt由Niels Provos和David Mazières于1999年专门为密码哈希设计。这个背景很重要。它不是为了快速而构建的——它是为了以受控、可调的方式缓慢而构建的。
bcrypt背后的核心洞察是,对于密码来说,快速哈希是一种负担。像SHA-256这样的通用哈希在现代硬件上可以每秒计算数十亿个哈希。这对校验和和数据完整性很好。对密码来说很糟糕,因为攻击者窃取密码数据库后可以用普通GPU每秒尝试数十亿次猜测。
bcrypt通过引入成本因子来解决这个问题——一个控制算法工作量的参数。你决定哈希的缓慢程度。硬件每年变得更快,所以你可以随时间增加成本因子来保持领先。
bcrypt实际上如何工作
当你调用bcrypt.hash("mypassword", 10)时,内部发生了几件事:
- 生成一个随机的16字节盐值。 这个盐值对每个哈希操作是唯一的。
- 密码和盐值被输入到Eksblowfish密钥设置中。 这是bcrypt的核心算法——Blowfish密码的修改版,具有昂贵的密钥扩展阶段。
- 密钥设置重复2^成本次。 成本因子10时是1,024次,成本12时是4,096次。每次增量使工作量翻倍。
- 结果哈希被编码为60字符的字符串,其中包含算法版本、成本因子、盐值和哈希。
输出看起来像这样:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
分解:
$2b$— bcrypt版本10— 成本因子- 接下来22个字符 — base64编码的盐值
- 剩余字符 — 哈希
因为盐值嵌入在哈希字符串中,你不需要单独存储它。bcrypt.compare()函数从存储的哈希中提取盐值,并用它来哈希输入密码——不需要手动盐值管理。
为什么快速哈希函数是错误的工具
开发者有时用MD5、SHA-1或SHA-256存储密码。这些算法广泛可用且被充分理解,所以选择看起来合理。但事实并非如此。
问题不是这些算法对其预期目的来说是破损的。问题是它们被设计为快速,而快速正是密码存储所错误的特性。
2012年,安全研究员Jeremi Gosney在一小时内破解了一个泄露的640万SHA-1密码哈希数据库中的90%。使用现代硬件和Hashcat等工具,拥有好GPU的攻击者可以每秒测试超过100亿个MD5哈希。SHA-256也好不了多少——每秒约40亿个。
成本10的bcrypt将其降低到每秒约20,000次尝试。这就是全部意义所在。
选择正确的成本因子
成本因子是你使用bcrypt时最重要的调整决策。
成本10是广泛推荐的默认值。在现代服务器上产生约100-300ms的哈希。对于登录的用户来说感觉不到,但攻击者每个核心每秒只能测试几千个密码。
成本12与成本10相比大约将计算时间增加四倍。对于有较高安全要求的应用程序——银行、医疗、处理敏感财务数据的任何东西——在可以接受400-1000ms登录时间的情况下是合理的选择。
成本14及以上对Web应用程序来说通常是过度的。在成本14时,哈希需要几秒钟,这会明显影响用户体验。
生产环境中永远不要低于8。 一些旧教程将成本5或6作为示例展示。这些值太快,提供的保护不足。
实用规则:在你的实际硬件上进行基准测试,选择在预期并发负载下保持登录时间在250ms以下的最高成本因子。然后随着硬件的改进每两到三年重新检视。
在Node.js中实现bcrypt
const bcrypt = require('bcrypt');
const COST_FACTOR = 10;
// 哈希密码
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, COST_FACTOR);
return hash; // 将此字符串存储在数据库中
}
// 登录时验证
async function verifyPassword(plaintext, storedHash) {
const match = await bcrypt.compare(plaintext, storedHash);
return match; // true 或 false
}
// 使用示例
const hash = await hashPassword('hunter2');
console.log(hash); // $2b$10$...
const valid = await verifyPassword('hunter2', hash);
console.log(valid); // true
值得注意的是:bcrypt.hash()是异步的,意味着它在线程池中运行,不会阻塞你的事件循环。在生产环境中使用异步版本——同步的bcrypt.hashSync()在负载下会使服务器停滞。
在Python中实现bcrypt
import bcrypt
COST_FACTOR = 10
def hash_password(plaintext: str) -> bytes:
password_bytes = plaintext.encode('utf-8')
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=COST_FACTOR))
return hashed
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
password_bytes = plaintext.encode('utf-8')
return bcrypt.checkpw(password_bytes, stored_hash)
# 使用
hashed = hash_password("hunter2")
print(hashed) # b'$2b$10$...'
print(verify_password("hunter2", hashed)) # True
print(verify_password("wrong", hashed)) # False
在PHP中实现bcrypt
<?php
$options = ['cost' => 10];
// 哈希
$hash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// 验证
$valid = password_verify($plaintext, $hash);
// 检查哈希是否需要升级(例如提高了成本因子)
if (password_needs_rehash($hash, PASSWORD_BCRYPT, $options)) {
$newHash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// 将$newHash保存到数据库
}
?>
在Go中实现bcrypt
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
破坏bcrypt的常见错误
每次登录验证时重新哈希密码。 这是一个重大的性能错误。当用户登录时,应该使用bcrypt.compare()将密码与存储的哈希进行比较。不要重新哈希密码然后比较两个哈希——那永远不会匹配,因为每次都会生成新的随机盐值。
存储明文密码。 显而易见,但确实会发生。
使用MD5或SHA进行密码存储。 即使手动添加盐值,快速哈希给攻击者提供了太多吞吐量。
不处理72字节限制。 bcrypt在72字节处静默截断输入。标准缓解措施是先用SHA-256哈希密码,然后用bcrypt哈希十六进制或二进制输出。
使用太低的成本因子。 如果使用成本4是因为它更快,那你就错过了重点。缓慢是安全属性,不是错误。
什么时候使用Argon2
bcrypt已经是25年的安全默认值,但它不是现代最佳选择。Argon2——具体来说是Argon2id——在2015年赢得了Password Hashing Competition,并解决了bcrypt的主要弱点。
bcrypt是CPU绑定的:拥有GPU的攻击者可以相对廉价地并行化破解,因为算法不需要太多内存。Argon2是内存密集型的:每次哈希操作需要可配置数量的RAM。GPU每个计算核心的内存比CPU少得多,因此内存密集型算法大幅拉平了竞争环境。
总结
- bcrypt的强度来自故意的缓慢,而不是密码复杂性
- 始终使用
bcrypt.compare()(或你语言的等效函数)进行验证——永远不要哈希后比较 - 成本因子10是大多数应用程序的正确默认值;每隔几年重新检视
- bcrypt自动处理盐值——你不需要管理盐值
- 注意异常长密码的72字节截断限制
- 对于新项目,Argon2id值得考虑;对于现有bcrypt系统,保持不变
密码存储是少数几个安全选项——具有合理成本因子的bcrypt——也简单易于正确实现的领域之一。这个空间中的大多数漏洞来自忽视建议,而不是算法本身的缺陷。