正在加载,请稍候…

如何安全存储密码:2025 年开发者检查清单

一份逐步检查清单,教你安全存储用户密码——涵盖为何 MD5 和 SHA-256 不适合密码,bcrypt 和 Argon2 的工作原理,以及应使用的确切代码模式

如何安全存储密码:2025 年开发者检查清单

检查清单先行

在解释之前,以下是正确存储密码必须做的事情。每一项都很重要。

  • 绝不存储明文密码
  • 绝不单独使用 MD5、SHA-1 或 SHA-256 处理密码
  • 使用 bcrypt、Argon2id 或 scrypt——而非通用哈希
  • 让库自动生成盐——不要手动操作
  • 设置工作因子,使哈希计算在你的服务器上耗时 100–300 毫秒
  • 强制密码最小长度为 8 个字符,最大长度为 72 个字符(bcrypt 限制)
  • 在服务器端哈希密码,而非客户端
  • 绝不记录密码,即使在错误消息中
  • 在登录端点实施速率限制和账户锁定

如何安全存储密码:2025 年开发者检查清单插图

为什么 MD5 和 SHA-256 不适合密码

这是密码安全中最容易被误解的一点。MD5、SHA-1 和 SHA-256 很快。非常快。现代 GPU 每秒可以计算 100 亿次 MD5 哈希。这意味着破解者可以在大约 30 秒内针对 MD5 哈希数据库测试所有 8 位小写密码。

密码哈希算法如 bcrypt、Argon2 和 scrypt 故意设计得很慢。它们可调优,你可以让哈希计算恰好花费服务器能容忍的时间(通常 100–300 毫秒)。同样的 30 秒破解将变成 3000 年。

原始 SHA-256 的另一个问题:没有盐。如果两个用户密码相同,他们的哈希值相同。攻击者可以通过在彩虹表中找到一个匹配的哈希值,一次性破解数千个账户。

正确的算法

bcrypt

支持最广泛的选择。在每种主流语言中都可使用。盐是内置的——bcrypt 自动生成盐并将其存储在哈希字符串中。

工作因子(rounds)是 2 的幂:rounds=12 表示 2¹² = 4096 次迭代。随着硬件性能提升而增加。行业标准起点是 10–12。

// Node.js with bcryptjs
const bcrypt = require('bcryptjs');

// 注册时哈希
const hash = await bcrypt.hash(plainPassword, 12);  // 12 rounds
// 将 'hash' 存入数据库

// 登录时验证
const isMatch = await bcrypt.compare(plainPassword, storedHash);
// 返回 true/false——内置了时序安全比较
# Python with bcrypt
import bcrypt

# 注册时哈希
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode(), salt)
# 将 'hashed'(字节)存入数据库

# 登录时验证
is_match = bcrypt.checkpw(password.encode(), stored_hash)

Bcrypt 限制: 最大输入为 72 字节。超过 72 个字符的密码会被静默截断。如果你想支持更长的密码,先用 SHA-256 预哈希并 base64 编码,再传给 bcrypt:

const crypto = require('crypto');
const prehash = crypto.createHash('sha256').update(password).digest('base64');
const hash = await bcrypt.hash(prehash, 12);

如何安全存储密码:2025 年开发者检查清单插图

Argon2id

2015 年密码哈希竞赛的获胜者。比 bcrypt 更可配置:你可以分别调整内存使用量(RAM)、并行度(CPU 线程)和迭代次数。Argon2id 是混合变体——既能抵抗 GPU 攻击,也能抵抗侧信道攻击。

// Node.js with argon2
const argon2 = require('argon2');

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,    // 64 MB
  timeCost: 3,          // 3 次迭代
  parallelism: 4,       // 4 个线程
});

const isMatch = await argon2.verify(hash, password);
# Python with argon2-cffi
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hash = ph.hash(password)

try:
    ph.verify(hash, password)
    # 登录成功
    if ph.check_needs_rehash(hash):
        new_hash = ph.hash(password)
        # 用 new_hash 更新数据库
except Exception:
    pass  # 登录失败

scrypt

自 Python 3.6 起内置于标准库,Node.js 的 crypto 模块也有。如果你无法添加外部依赖,这是个不错的选择。

// Node.js built-in crypto
const crypto = require('crypto');
const util = require('util');
const scrypt = util.promisify(crypto.scrypt);

const salt = crypto.randomBytes(32);
const hash = await scrypt(password, salt, 64);
// 存储:salt.toString('hex') + ':' + hash.toString('hex')

算法对比

算法 内置盐 GPU 抵抗性 内存硬度 推荐?
MD5 非常弱 ❌ 绝不
SHA-256 非常弱 ❌ 绝不
bcrypt 中等 ✅ 是
scrypt 手动 ✅ 是
Argon2id ✅ 最佳选择
PBKDF2 手动 中等 ✅ 如果其他不可用

如何安全存储密码:2025 年开发者检查清单插图

设置正确的工作因子

目标:在您的生产服务器上,哈希一个密码应耗时 100–300 毫秒。太快 = 容易破解。太慢 = 攻击者通过垃圾登录端点造成 DoS 风险。

// 基准测试脚本——在您的生产硬件上运行
const bcrypt = require('bcryptjs');
for (let rounds = 10; rounds <= 14; rounds++) {
  const start = Date.now();
  await bcrypt.hash('benchmark', rounds);
  console.log(`rounds=${rounds}: ${Date.now() - start}ms`);
}
// 选择低于 300ms 的最高 rounds 值

现代服务器上的典型结果:

  • rounds=10: ~65ms
  • rounds=12: ~250ms ← 良好的默认值
  • rounds=14: ~1000ms ← 对大多数应用来说太慢

数据库应存储的内容

对于 bcrypt,整个哈希字符串包含算法、版本、轮数、盐和哈希——全部在一个字符串中,例如:

$2b$12$LrhasAakElf4YCRZzEJxXONMpBRrAVbKCMSJIJSDJSJDHEIJJ

将此整个字符串存储在一个 VARCHAR(255)TEXT 列中。切勿单独存储盐。切勿在单独的列中存储任何关于算法的元数据——它已经在哈希字符串中了。

检查现有密码存储

如果您正在审计现有应用程序:

-- PostgreSQL:查找未哈希的密码(明文少于 50 字符,无哈希前缀)
SELECT COUNT(*) FROM users WHERE LENGTH(password_hash) < 20;

-- 查找 MD5 哈希(32 个十六进制字符)
SELECT COUNT(*) FROM users WHERE password_hash ~ '^[a-f0-9]{32}
#39;; -- 查找 SHA-256 哈希(64 个十六进制字符) SELECT COUNT(*) FROM users WHERE password_hash ~ '^[a-f0-9]{64}
#39;; -- 查找 bcrypt 哈希(正确) SELECT COUNT(*) FROM users WHERE password_hash LIKE '$2%';

→ 在浏览器中使用 Bcrypt 工具 测试 bcrypt 哈希生成和验证——有助于验证您的库生成的内容。