Bcrypt 是业界最受信赖的密码哈希算法之一,专为保护用户凭证免受暴力破解攻击而设计。与 MD5 或 SHA-256 等快速哈希算法不同,Bcrypt 故意设计得很慢,并内置盐值生成机制,使其成为安全存储密码的理想选择。本文将深入探讨 Bcrypt 的工作原理、配置方法和实施最佳实践。

目录

核心要点

  • 内置盐值:Bcrypt 自动为每个密码生成唯一的 128 位盐值,无需手动管理盐值。
  • 自适应成本因子:工作因子(轮数)可随时间增加,以跟上硬件性能的提升。
  • 故意设计得慢:Bcrypt 故意计算密集,使暴力破解攻击变得不切实际。
  • 哈希结构:Bcrypt 哈希包含算法版本、成本因子、盐值和哈希值,全部在一个字符串中。
  • 推荐成本因子:大多数应用使用成本因子 10-12(目标哈希时间约 100ms-300ms)。
  • 行业标准:Bcrypt 仍然是密码哈希的可靠选择,尽管新项目推荐使用 Argon2。

需要生成或验证 Bcrypt 哈希?试试我们的免费在线工具:

立即体验 Bcrypt 生成器

什么是Bcrypt?

Bcrypt 是由 Niels Provos 和 David Mazières 于 1999 年设计的密码哈希函数,基于 Blowfish 密码算法。"bcrypt"这个名字来源于"Blowfish crypt",反映了其密码学基础。

为什么要创建Bcrypt

传统的哈希函数如 MD5 和 SHA-1 被设计得很快,这对数据完整性检查很好,但对密码存储来说很糟糕。快速哈希意味着攻击者每秒可以尝试数十亿次密码猜测。

Bcrypt 通过以下方式解决这个问题:

  1. 故意设计得慢:每次哈希计算都需要大量时间
  2. 可配置:可以通过成本因子调整计算速度
  3. 盐值集成:每个密码自动获得唯一的盐值

核心特性

特性 描述
算法 基于 Blowfish 密码(Eksblowfish)
输出大小 184 位(24 字节)哈希 + 128 位盐值
盐值 128 位,自动生成
成本因子 可配置(4-31),指数级工作量增加
字符串格式 $2a$$2b$$2y$ 前缀

Bcrypt工作原理

Bcrypt 的安全性来自其独特的密码哈希方法。以下是处理流程:

flowchart TD A[密码输入] --> B[生成随机盐值] B --> C["组合密码和盐值"] C --> D[密钥设置阶段] D --> E["昂贵的密钥调度 (2^cost 次迭代)"] E --> F[加密魔术值] F --> G[最终哈希输出] style A fill:#e1f5fe,stroke:#01579b style B fill:#fff3e0,stroke:#e65100 style E fill:#fce4ec,stroke:#c2185b style G fill:#e8f5e9,stroke:#2e7d32

分步过程

  1. 盐值生成:生成密码学安全的 128 位随机盐值
  2. 密钥设置:使用密码和盐值初始化 Eksblowfish 密码
  3. 昂贵的密钥调度:密钥调度重复 2^cost 次
  4. 加密:魔术值("OrpheanBeholderScryDoubt")被加密 64 次
  5. 输出:盐值和结果哈希组合成最终字符串

为什么相同密码产生不同哈希

一个常见问题是:"为什么对同一个密码哈希两次会得到不同的结果?"

这是因为 Bcrypt 为每次哈希操作生成新的随机盐值。盐值随后被嵌入到输出字符串中,因此验证时可以提取它并重现相同的哈希。

code
密码: "mypassword"

第一次哈希:  $2b$10$N9qo8uLOickgx2ZMRZoMy.MqrqQb9lYz6H8Kj7OvBOyj5uYjiPWmu
第二次哈希: $2b$10$ZGdlbGVwaGFudHNhcmVjb.7xJ8L9KjQvMnOpRsTuVwXyZaBcDeFgH

两个都是 "mypassword" 的有效哈希 - 不同的盐值,相同的密码!

理解成本因子

成本因子(也称为"轮数"或"工作因子")决定了哈希过程的计算密集程度。它以 2 的幂次表示。

成本因子影响

成本因子 迭代次数 大约时间*
4 16 ~1ms
8 256 ~10ms
10 1,024 ~100ms
12 4,096 ~300ms
14 16,384 ~1s
16 65,536 ~4s

*时间是近似值,因硬件而异

选择合适的成本因子

flowchart LR A[开始] --> B{"哈希时间目标?"} B -->|小于100ms| C["成本因子 10"] B -->|100-300ms| D["成本因子 11-12"] B -->|大于300ms| E["成本因子 13+"] C --> F["适合高流量"] D --> G["推荐平衡"] E --> H["最高安全性"] style D fill:#e8f5e9,stroke:#2e7d32 style G fill:#e8f5e9,stroke:#2e7d32

建议:

  • 开发/测试:成本因子 4-6(快速迭代)
  • 生产环境(标准):成本因子 10-12(100-300ms)
  • 高安全性:成本因子 13-14(如果用户体验允许)

随时间升级成本因子

随着硬件改进,你应该增加成本因子。以下是一个策略:

javascript
// 在登录时检查哈希是否需要升级
async function loginAndUpgrade(password, storedHash) {
  const isValid = await bcrypt.compare(password, storedHash);
  
  if (isValid) {
    const currentCost = parseInt(storedHash.split('$')[2]);
    const targetCost = 12;
    
    if (currentCost < targetCost) {
      // 使用更高的成本因子重新哈希
      const newHash = await bcrypt.hash(password, targetCost);
      await updateUserHash(newHash);
    }
  }
  
  return isValid;
}

Bcrypt哈希结构解析

Bcrypt 哈希字符串包含验证所需的所有信息:

code
$2b$12$N9qo8uLOickgx2ZMRZoMyeKj7OvBOyj5uYjiPWmuabcdefghijk
│ │  │  │                     │
│ │  │  │                     └── 哈希值(31个字符)
│ │  │  └── 盐值(22个字符)
│ │  └── 成本因子(2位数字)
│ └── 算法版本
└── 前缀标记

算法版本

版本 描述
$2$ 原始规范(已过时)
$2a$ 修复了bug,最常见
$2b$ 修复了无符号字符bug(2014年)
$2y$ PHP特定,等同于 $2b$

解码真实哈希

让我们分解这个哈希:$2b$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa

组件 含义
算法 $2b$ Bcrypt 版本 2b
成本 10 2^10 = 1,024 次迭代
盐值 vI8aWBnW3fID.ZQ4/zo1G. 22字符 Base64 编码盐值
哈希 q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa 31字符 Base64 编码哈希

Bcrypt与其他算法对比

对比表

特性 Bcrypt Argon2 Scrypt PBKDF2
年份 1999 2015 2009 2000
内存密集
GPU抗性 中等
并行性 可配置
OWASP推荐 ✅(首选)
成熟度

何时使用哪个

flowchart TD A[选择算法] --> B{新项目?} B -->|是| C{内存可用?} B -->|否| D["保持当前算法"] C -->|是| E[Argon2id] C -->|否| F[Bcrypt] D --> G{"使用MD5或SHA?"} G -->|是| H["迁移到Bcrypt或Argon2"] G -->|否| I["如需要增加成本因子"] style E fill:#e8f5e9,stroke:#2e7d32 style F fill:#e8f5e9,stroke:#2e7d32

总结:

  • 新项目:考虑 Argon2id(如果有库支持)
  • 现有项目:Bcrypt 仍然很优秀
  • 遗留系统:从 MD5/SHA 迁移到 Bcrypt
  • 资源受限:Bcrypt(内存需求较低)

代码示例

Node.js (bcryptjs)

javascript
const bcrypt = require('bcryptjs');

// 生成哈希
async function hashPassword(password) {
  const saltRounds = 12;
  const hash = await bcrypt.hash(password, saltRounds);
  return hash;
}

// 验证密码
async function verifyPassword(password, hash) {
  const isMatch = await bcrypt.compare(password, hash);
  return isMatch;
}

// 使用示例
async function main() {
  const password = 'mySecurePassword123';
  
  // 哈希密码
  const hash = await hashPassword(password);
  console.log('哈希:', hash);
  // 输出: $2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.G5e0oO0oa5m6Wy
  
  // 验证正确密码
  const isValid = await verifyPassword(password, hash);
  console.log('有效:', isValid); // true
  
  // 验证错误密码
  const isInvalid = await verifyPassword('wrongPassword', hash);
  console.log('无效:', isInvalid); // false
}

main();

Python

python
import bcrypt

def hash_password(password: str) -> bytes:
    """使用bcrypt哈希密码"""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed

def verify_password(password: str, hashed: bytes) -> bool:
    """验证密码与哈希是否匹配"""
    return bcrypt.checkpw(password.encode('utf-8'), hashed)

# 使用示例
password = "mySecurePassword123"

# 哈希
hashed = hash_password(password)
print(f"哈希: {hashed.decode()}")

# 验证
is_valid = verify_password(password, hashed)
print(f"有效: {is_valid}")  # True

is_invalid = verify_password("wrongPassword", hashed)
print(f"无效: {is_invalid}")  # False

Java (Spring Security)

java
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BcryptExample {
    public static void main(String[] args) {
        // 创建强度(成本因子)为12的编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
        
        String password = "mySecurePassword123";
        
        // 哈希密码
        String hash = encoder.encode(password);
        System.out.println("哈希: " + hash);
        
        // 验证密码
        boolean isValid = encoder.matches(password, hash);
        System.out.println("有效: " + isValid); // true
        
        boolean isInvalid = encoder.matches("wrongPassword", hash);
        System.out.println("无效: " + isInvalid); // false
    }
}

Go

go
package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

func hashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    return string(bytes), err
}

func verifyPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "mySecurePassword123"
    
    // 哈希
    hash, _ := hashPassword(password)
    fmt.Println("哈希:", hash)
    
    // 验证
    isValid := verifyPassword(password, hash)
    fmt.Println("有效:", isValid) // true
    
    isInvalid := verifyPassword("wrongPassword", hash)
    fmt.Println("无效:", isInvalid) // false
}

安全最佳实践

应该做的 ✅

  1. 生产系统使用成本因子 10-12
  2. 存储完整的哈希字符串(包含盐值和版本)
  3. 使用常量时间比较(内置于 bcrypt 库中)
  4. 随着硬件改进升级成本因子
  5. 传输密码时使用 HTTPS
  6. 在登录端点实施速率限制

不应该做的 ❌

  1. 生产环境不要使用低于 10 的成本因子
  2. 不要单独存储盐值(它在哈希中)
  3. 不要在哈希前截断密码
  4. 不要将 bcrypt 用于非密码数据(使用 SHA-256)
  5. 不要以明文记录密码或哈希
  6. 不要自己实现 bcrypt(使用成熟的库)

密码长度考虑

Bcrypt 的最大输入长度为 72 字节。对于更长的密码:

javascript
// 使用 SHA-256 预哈希长密码
const crypto = require('crypto');
const bcrypt = require('bcryptjs');

async function hashLongPassword(password) {
  // 如果密码可能超过72字节,先预哈希
  const preHash = crypto
    .createHash('sha256')
    .update(password)
    .digest('base64');
  
  return bcrypt.hash(preHash, 12);
}

常见问题

Q1: 为什么相同密码产生不同的哈希?

Bcrypt 为每次哈希操作生成唯一的随机盐值。这个盐值被嵌入到输出字符串中。验证时,盐值被提取出来用于重现哈希。这种设计防止了彩虹表攻击。

Q2: 应该使用什么成本因子?

对于大多数应用,使用成本因子 10-12,目标哈希时间 100-300ms。在你的生产硬件上测试并相应调整。如果用户体验影响可接受,高安全性应用可以使用 13-14。

Q3: Bcrypt 在 2026 年还安全吗?

是的,Bcrypt 仍然安全,并被 OWASP 推荐。虽然新项目首选 Argon2,但成本因子 10+ 的 Bcrypt 对暴力破解攻击提供了出色的保护。

Q4: 可以将 Bcrypt 用于 API 密钥或令牌吗?

不可以,Bcrypt 是为密码验证设计的,不是通用哈希。对于 API 密钥或令牌,使用 SHA-256 或 HMAC-SHA256。Bcrypt 的慢速会为高频操作带来性能问题。

Q5: 如何从 MD5 迁移到 Bcrypt?

在用户登录时,先验证 MD5,然后用 Bcrypt 重新哈希:

javascript
async function migrateHash(password, oldMd5Hash) {
  const md5 = crypto.createHash('md5').update(password).digest('hex');
  
  if (md5 === oldMd5Hash) {
    // 密码正确,创建新的 bcrypt 哈希
    const newHash = await bcrypt.hash(password, 12);
    await updateUserHash(newHash);
    return true;
  }
  return false;
}

总结

Bcrypt 仍然是最可靠的密码哈希算法之一。其内置的盐值生成、可配置的成本因子和经过实战检验的实现,使其成为保护用户凭证的绝佳选择。

要记住的关键点:

  • 生产环境始终使用成本因子 10+
  • 哈希字符串包含验证所需的一切
  • 随着硬件改进升级成本因子
  • 对于有内存密集需求的新项目考虑 Argon2

准备好生成或验证 Bcrypt 哈希了吗?试试我们的免费在线工具:

立即生成 Bcrypt 哈希

更多安全工具,请查看我们的 Hash 计算器密码生成器