Bcrypt 是业界最受信赖的密码哈希算法之一,专为保护用户凭证免受暴力破解攻击而设计。与 MD5 或 SHA-256 等快速哈希算法不同,Bcrypt 故意设计得很慢,并内置盐值生成机制,使其成为安全存储密码的理想选择。本文将深入探讨 Bcrypt 的工作原理、配置方法和实施最佳实践。
目录
核心要点
- 内置盐值:Bcrypt 自动为每个密码生成唯一的 128 位盐值,无需手动管理盐值。
- 自适应成本因子:工作因子(轮数)可随时间增加,以跟上硬件性能的提升。
- 故意设计得慢:Bcrypt 故意计算密集,使暴力破解攻击变得不切实际。
- 哈希结构:Bcrypt 哈希包含算法版本、成本因子、盐值和哈希值,全部在一个字符串中。
- 推荐成本因子:大多数应用使用成本因子 10-12(目标哈希时间约 100ms-300ms)。
- 行业标准:Bcrypt 仍然是密码哈希的可靠选择,尽管新项目推荐使用 Argon2。
需要生成或验证 Bcrypt 哈希?试试我们的免费在线工具:
什么是Bcrypt?
Bcrypt 是由 Niels Provos 和 David Mazières 于 1999 年设计的密码哈希函数,基于 Blowfish 密码算法。"bcrypt"这个名字来源于"Blowfish crypt",反映了其密码学基础。
为什么要创建Bcrypt
传统的哈希函数如 MD5 和 SHA-1 被设计得很快,这对数据完整性检查很好,但对密码存储来说很糟糕。快速哈希意味着攻击者每秒可以尝试数十亿次密码猜测。
Bcrypt 通过以下方式解决这个问题:
- 故意设计得慢:每次哈希计算都需要大量时间
- 可配置:可以通过成本因子调整计算速度
- 盐值集成:每个密码自动获得唯一的盐值
核心特性
| 特性 | 描述 |
|---|---|
| 算法 | 基于 Blowfish 密码(Eksblowfish) |
| 输出大小 | 184 位(24 字节)哈希 + 128 位盐值 |
| 盐值 | 128 位,自动生成 |
| 成本因子 | 可配置(4-31),指数级工作量增加 |
| 字符串格式 | $2a$、$2b$ 或 $2y$ 前缀 |
Bcrypt工作原理
Bcrypt 的安全性来自其独特的密码哈希方法。以下是处理流程:
分步过程
- 盐值生成:生成密码学安全的 128 位随机盐值
- 密钥设置:使用密码和盐值初始化 Eksblowfish 密码
- 昂贵的密钥调度:密钥调度重复 2^cost 次
- 加密:魔术值("OrpheanBeholderScryDoubt")被加密 64 次
- 输出:盐值和结果哈希组合成最终字符串
为什么相同密码产生不同哈希
一个常见问题是:"为什么对同一个密码哈希两次会得到不同的结果?"
这是因为 Bcrypt 为每次哈希操作生成新的随机盐值。盐值随后被嵌入到输出字符串中,因此验证时可以提取它并重现相同的哈希。
密码: "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 |
*时间是近似值,因硬件而异
选择合适的成本因子
建议:
- 开发/测试:成本因子 4-6(快速迭代)
- 生产环境(标准):成本因子 10-12(100-300ms)
- 高安全性:成本因子 13-14(如果用户体验允许)
随时间升级成本因子
随着硬件改进,你应该增加成本因子。以下是一个策略:
// 在登录时检查哈希是否需要升级
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 哈希字符串包含验证所需的所有信息:
$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推荐 | ✅ | ✅(首选) | ✅ | ✅ |
| 成熟度 | 高 | 中 | 高 | 高 |
何时使用哪个
总结:
- 新项目:考虑 Argon2id(如果有库支持)
- 现有项目:Bcrypt 仍然很优秀
- 遗留系统:从 MD5/SHA 迁移到 Bcrypt
- 资源受限:Bcrypt(内存需求较低)
代码示例
Node.js (bcryptjs)
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
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)
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
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
}
安全最佳实践
应该做的 ✅
- 生产系统使用成本因子 10-12
- 存储完整的哈希字符串(包含盐值和版本)
- 使用常量时间比较(内置于 bcrypt 库中)
- 随着硬件改进升级成本因子
- 传输密码时使用 HTTPS
- 在登录端点实施速率限制
不应该做的 ❌
- 生产环境不要使用低于 10 的成本因子
- 不要单独存储盐值(它在哈希中)
- 不要在哈希前截断密码
- 不要将 bcrypt 用于非密码数据(使用 SHA-256)
- 不要以明文记录密码或哈希
- 不要自己实现 bcrypt(使用成熟的库)
密码长度考虑
Bcrypt 的最大输入长度为 72 字节。对于更长的密码:
// 使用 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 重新哈希:
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 哈希了吗?试试我们的免费在线工具: