ToolPal
键盘上的组合锁,象征密码安全

bcrypt密码哈希:开发者实用指南

📷 Pixabay / Pexels

bcrypt密码哈希:开发者实用指南

关于密码安全bcrypt的一切——成本因子、在Node.js/Python/PHP中的实现,以及要避免的常见错误。

D作者: Daniel Park2026年4月7日2分钟阅读

如果你曾经需要存储用户密码,你可能被告知要"使用bcrypt"。这是二十多年来的标准建议,有充分的理由。但理解为什么它有效——以及如何正确使用它——才是安全实现与看起来安全的实现之间的区别。

本指南涵盖所有实用内容:bcrypt实际如何工作、选择哪个成本因子、如何在多种语言中实现,以及悄然破坏密码安全的错误。

bcrypt是什么,为什么存在

bcrypt由Niels Provos和David Mazières于1999年专门为密码哈希设计。这个背景很重要。它不是为了快速而构建的——它是为了以受控、可调的方式缓慢而构建的。

bcrypt背后的核心洞察是,对于密码来说,快速哈希是一种负担。像SHA-256这样的通用哈希在现代硬件上可以每秒计算数十亿个哈希。这对校验和和数据完整性很好。对密码来说很糟糕,因为攻击者窃取密码数据库后可以用普通GPU每秒尝试数十亿次猜测。

bcrypt通过引入成本因子来解决这个问题——一个控制算法工作量的参数。你决定哈希的缓慢程度。硬件每年变得更快,所以你可以随时间增加成本因子来保持领先。

bcrypt实际上如何工作

当你调用bcrypt.hash("mypassword", 10)时,内部发生了几件事:

  1. 生成一个随机的16字节盐值。 这个盐值对每个哈希操作是唯一的。
  2. 密码和盐值被输入到Eksblowfish密钥设置中。 这是bcrypt的核心算法——Blowfish密码的修改版,具有昂贵的密钥扩展阶段。
  3. 密钥设置重复2^成本次。 成本因子10时是1,024次,成本12时是4,096次。每次增量使工作量翻倍。
  4. 结果哈希被编码为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——也简单易于正确实现的领域之一。这个空间中的大多数漏洞来自忽视建议,而不是算法本身的缺陷。

常见问题

D

关于作者

Daniel Park

Senior frontend engineer based in Seoul. Seven years of experience building web applications at Korean SaaS companies, with a focus on developer tooling, web performance, and privacy-first architecture. Open-source contributor to the JavaScript ecosystem and founder of ToolPal.

了解更多

分享文章

XLinkedIn

相关文章