如何安全地哈希存储用户密码
绝对不要用 MD5 或 SHA256 存储密码
这是密码安全中最重要的一条规则:MD5、SHA1、SHA256 等通用哈希函数都不应该用于密码存储。原因是它们设计得太快了——每秒可以计算数十亿次哈希。这意味着攻击者拿到哈希后,可以在很短时间内通过暴力搜索或字典攻击找到原始密码。2012 年 LinkedIn 的数据泄露中,约 60% 的 SHA1 密码哈希在几天内被破解,因为许多用户使用了弱密码。
密码哈希的特殊要求
安全的密码存储需要满足三个核心要求:
- **慢速(Cost Factor):**哈希计算应该故意设计得很慢(通常数十到数百毫秒),让暴力破解代价高昂
- **盐值(Salt):**每个密码应使用唯一的随机盐值,防止彩虹表攻击和避免相同密码产生相同哈希
- **自适应(Adaptable):**随着硬件越来越快,可以增加计算成本参数(Cost Factor)来维持安全性
bcrypt:经典的密码哈希函数
bcrypt 由 Niels Provos 和 David Mazières 于 1999 年设计,是目前使用最广泛的密码哈希函数。它内置了盐值和代价因子(Cost Factor),通过 cost 参数控制计算复杂度:
// Node.js (using bcryptjs or bcrypt package)
const bcrypt = require('bcryptjs');
// Hashing a password
async function hashPassword(password) {
const saltRounds = 12; // Cost factor: 2^12 = 4096 iterations
const hash = await bcrypt.hash(password, saltRounds);
return hash;
// Returns: "$2b$12$[22-char salt][31-char hash]"
}
// Verifying a password
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match; // true or false
}
// Usage
const hash = await hashPassword('mypassword123');
// Store hash in database
const isValid = await verifyPassword('mypassword123', hash);
// true
cost 参数建议:对于大多数应用,12–14 是合适的选择(使哈希时间约为 100–300ms)。随着硬件升级,可以逐步提高此值。
Argon2:现代密码哈希的首选
Argon2 是 2015 年密码哈希竞赛(PHC)的获胜者,被认为是目前最安全的密码哈希算法。Argon2 有三个变体:Argon2i(抗侧信道攻击)、Argon2d(抗 GPU 攻击)、Argon2id(两者的结合,推荐)。与 bcrypt 相比,Argon2 还可以配置内存使用量,使其更难通过专用硬件(ASIC)并行破解。
// Python (using argon2-cffi)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # Iterations
memory_cost=65536, # 64 MB RAM usage
parallelism=2, # Parallel threads
hash_len=32, # Hash output length
salt_len=16 # Salt length
)
# Hash
hash = ph.hash("mypassword123")
# Returns: "$argon2id$v=19$m=65536,t=2,p=2$..."
# Verify
try:
ph.verify(hash, "mypassword123")
print("Password correct")
except:
print("Password incorrect")
PBKDF2:广泛支持的标准方案
PBKDF2(Password-Based Key Derivation Function 2)是 NIST 推荐且 FIPS 140-2 认可的密码哈希方案。虽然安全性不如 bcrypt 和 Argon2(没有内存复杂度),但在需要合规认证的环境中非常有用:
// Node.js (built-in crypto module)
const crypto = require('crypto');
const util = require('util');
const pbkdf2 = util.promisify(crypto.pbkdf2);
async function hashPassword(password) {
const salt = crypto.randomBytes(32).toString('hex');
const hash = await pbkdf2(password, salt, 310000, 32, 'sha256');
return `${salt}:${hash.toString('hex')}`;
}
async function verifyPassword(password, storedHash) {
const [salt, hash] = storedHash.split(':');
const newHash = await pbkdf2(password, salt, 310000, 32, 'sha256');
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), newHash);
}
时序安全比较
在验证密码(或任何秘密值)时,必须使用时序安全(Constant-Time)的比较函数,而不是普通的字符串相等比较(如 === 或 ==)。普通字符串比较在第一个不匹配字符处就会返回,攻击者可以通过测量响应时间来推断哈希值的内容(时序攻击)。Node.js 提供了 crypto.timingSafeEqual(),bcrypt 的 compare() 也内置了时序安全比较。
密码哈希最佳实践总结
- 使用 Argon2id(首选)、bcrypt(次选)或 PBKDF2(合规场景)
- 永远不要使用 MD5、SHA1、SHA256 单独哈希密码
- 永远不要自己实现密码哈希算法,使用经过审计的库
- 永远不要存储明文密码,也不要加密存储(加密意味着可以解密)
- 让哈希计算时间约为 100–300ms(既不让用户等太久,又让暴力破解代价高昂)
立即免费使用相关工具
免费使用 →