起因
老项目用 bcrypt(rounds=10) 存密码。安全审计建议升级到 argon2id:
bcrypt 是 1999 年算法,对 GPU 暴力破解抗性已弱。
argon2id 是 2015 年 Password Hashing Competition 冠军,
memory-hard,专门抗 GPU/ASIC 攻击。
OWASP 2024 推荐:新项目用 argon2id;老项目升级时 graceful 迁移。
解决方案
1. Python: argon2-cffi
uv add argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, InvalidHashError
# 单例 hasher
ph = PasswordHasher(
time_cost=3, # 迭代次数
memory_cost=65536, # 64 MB
parallelism=4, # 并行度
hash_len=32,
salt_len=16,
)
# 注册时
def register(email: str, password: str):
h = ph.hash(password) # 自动 salt
db.execute('INSERT INTO users (email, password_hash) VALUES (%s, %s)',
(email, h))
# 登录时
def verify(email: str, password: str) -> bool:
h = db.fetchone('SELECT password_hash FROM users WHERE email=%s', (email,))
if not h:
return False
try:
ph.verify(h[0], password)
# 如果参数升级了,自动 rehash
if ph.check_needs_rehash(h[0]):
new_h = ph.hash(password)
db.execute('UPDATE users SET password_hash=%s WHERE email=%s',
(new_h, email))
return True
except VerifyMismatchError:
return False
hash() 输出形如:
$argon2id$v=19$m=65536,t=3,p=4$<salt-b64>$<hash-b64>
参数(m / t / p)编码在 hash 里,verify 时不需要单独存。
2. 推荐参数
OWASP 2024 推荐(适合典型 web 服务器):
PasswordHasher(
time_cost=3,
memory_cost=12288, # 12 MB(低规模服务器)
parallelism=1,
)
memory_cost 单位 KiB。生产服务器 RAM 充足建议:
- 64-128 MB / hash
- time_cost 3-5
- parallelism 1(避免 thread 争抢)
调参思路:在你的服务器上测 ph.hash('test') 耗时,目标 250-500 ms。
太快 → 攻击者 GPU 一秒破百万;太慢 → 用户登录卡。
3. 渐进迁移:bcrypt → argon2id
用户表里 hash 共存:
from argon2 import PasswordHasher
import bcrypt
ph = PasswordHasher()
def verify(email, password):
h = db.get_user(email).password_hash
if h.startswith('$argon2'):
try: ph.verify(h, password); ok = True
except: ok = False
elif h.startswith('$2'): # bcrypt
ok = bcrypt.checkpw(password.encode(), h.encode())
else:
ok = False
if ok and not h.startswith('$argon2'):
# 用户登录成功 + 老 hash → 趁机升级
new_h = ph.hash(password)
db.update_user_hash(email, new_h)
return ok
不需要"全部用户重设密码",每次登录顺便升级。3-6 个月几乎全部用户
迁完。
4. Go: argon2id
go get github.com/alexedwards/argon2id
import "github.com/alexedwards/argon2id"
params := &argon2id.Params{
Memory: 64 * 1024,
Iterations: 3,
Parallelism: 1,
SaltLength: 16,
KeyLength: 32,
}
hash, err := argon2id.CreateHash(password, params)
// 存 hash 进 DB
// 验证
match, err := argon2id.ComparePasswordAndHash(password, hash)
5. Node.js: argon2
npm i argon2
import argon2 from 'argon2'
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
})
const ok = await argon2.verify(hash, password)
6. 用 passlib(Python 老项目)
Django 也用 passlib 兼容多算法:
# Django settings
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # 兼容老 hash
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
Django 验密时按顺序尝试匹配;用户登录成功后自动 rehash 到第一个
hasher(argon2id)。零代码迁移。
选择对比
| bcrypt | scrypt | PBKDF2 | argon2id | |
|---|---|---|---|---|
| 年代 | 1999 | 2009 | 2000 | 2015 |
| GPU 抗性 | 弱 | 中 | 弱 | 强(memory-hard) |
| 算法标准化 | 事实标准 | RFC 7914 | NIST | RFC 9106 |
| 推荐参数 | rounds=12+ | N=2^15+ | iters=600k+ | t=3,m=64MB |
| 库支持 | 极广 | 广 | 广 | 良好(增长中) |
2024 后新项目首选 argon2id;老项目迁移路径如上。
配套安全实践
1. 永远 hash + salt,不要明文
# ❌
db.execute('INSERT ... VALUES (%s, %s)', (email, password))
# ❌ 简单 hash(无 salt 易 rainbow table 攻击)
db.execute('INSERT ... VALUES (%s, %s)', (email, hashlib.sha256(password.encode()).hexdigest()))
# ✅
db.execute('INSERT ... VALUES (%s, %s)', (email, ph.hash(password)))
2. 等时比较
ph.verify() 内部用 constant-time compare。手写 hash == expected
有时序泄漏。
3. 不要 log 密码 / hash
# ❌
log.info('login attempt: user=%s pw=%s', email, password)
# ✅
log.info('login attempt: user=%s', email)
4. 限制登录失败
按 IP / 按 email 限速:
- 5 分钟内 5 次失败 → 拒绝 15 分钟
- 配合 captcha
5. 强密码策略
不强求复杂度,但建议:
- 最少 12 字符
- 用 zxcvbn / haveibeenpwned API 检查"是否已泄漏"
import requests
def is_pwned(password: str) -> bool:
sha = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha[:5], sha[5:]
r = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
return suffix in r.text
只发 prefix 5 位(k-anonymity),不泄漏完整 hash。
6. 不要"密码找回 = 显示原密码"
如果你能给用户发"你的密码是 xxx",说明密码明文存。删库跑路风险。
正确做法:reset 链接让用户重设新密码。
效果
我们升级后:
- 5000 用户在 4 个月内 95% 自然 rehash 到 argon2id
- 安全审计 pass
- 单次登录验证延迟从 50ms → 350ms(增加但仍可接受)
- 服务器 RAM 占用增加 ~50MB / 并发 login(PasswordHasher 实例)
踩过的坑
-
memory_cost 单位是 KiB:写 65536 是 64 MB,不是 64 KB。
写错值参数提示模糊。 -
每个 worker 都创建 hasher:PasswordHasher() 实例化贵。
全局单例。 -
rehash 升级时不告诉用户:用户体验无感(好事)。但要 log
"user X migrated to argon2id" 供审计。 -
password 长度无限:Argon2 自身能处理任意长度,但 DB column
设 VARCHAR(255) 限制 hash 输出(~100 字符)+ 限输入密码 ≤ 128 字符
防 DoS(超长密码 hash 时间长)。 -
跨实例参数不一致:你的 web server 用 64MB,别人在 mobile API
用 12MB,同密码 verify 应该都 work(参数编码在 hash 里)。
但 hash 操作如果 mobile API 太慢,统一参数到最小公倍数。
登录后参与评论。