密码学基础:哈希、加盐与密钥派生
网络安全系列 · 认证与授权篇
密码学是现代网络安全的基石。从登录密码的存储到 HTTPS 握手,从数字签名到区块链,密码学无处不在。但很多开发者对密码学的理解停留在"用 MD5 哈希一下"的层面,这恰恰是安全漏洞的温床。
本文将深入讲解密码学的三大核心概念:哈希函数、加盐、密钥派生函数(KDF),帮助你理解为什么 MD5 不能用于密码存储,以及现代系统应该如何正确处理用户凭证。
一、哈希函数:从任意数据到固定指纹
1.1 什么是哈希函数
哈希函数(Hash Function)是一种将任意长度的输入数据映射为固定长度输出的算法。这个输出通常被称为哈希值、摘要或指纹。
理想的哈希函数具有以下特性:
- 确定性:相同输入必定产生相同输出
- 快速计算:计算哈希值的速度要快
- 单向性:从哈希值无法反推出原始数据(不可逆)
- 抗碰撞性:很难找到两个不同的输入产生相同的哈希值
- 雪崩效应:输入微小的改动会导致输出完全改变
1.2 常见哈希算法
| 算法 | 输出长度 | 安全性 | 适用场景 |
|---|---|---|---|
| MD5 | 128 bit | ❌ 已破解 | 文件校验(不推荐) |
| SHA-1 | 160 bit | ❌ 已破解 | 旧系统兼容 |
| SHA-256 | 256 bit | ✅ 安全 | 数字签名、区块链 |
| SHA-3 | 可变 | ✅ 安全 | 高安全场景 |
| BLAKE2/3 | 可变 | ✅ 安全 | 高性能场景 |
1.3 为什么 MD5 不能用于密码存储
MD5 在密码存储场景中有两个致命缺陷:
第一,速度太快。
MD5 的设计初衷是快速计算文件校验和,而不是安全存储密码。现代 GPU 可以以每秒数十亿次的速度计算 MD5 哈希。这意味着一个 8 位纯数字密码的 MD5 值,在普通显卡上几分钟就能穷举完毕。
第二,碰撞攻击已实际可行。
2004 年,王小云教授团队首次在可计算时间内找到 MD5 的碰撞。2013 年,研究人员利用 MD5 碰撞伪造了符合 CA 标准的 TLS 证书。
# 错误示范:直接用 MD5 存储密码
import hashlib
def hash_password_wrong(password):
return hashlib.md5(password.encode()).hexdigest()
# 攻击者拿到数据库后,用彩虹表秒破
# 或者直接用 hashcat -m 0 暴力破解1.4 哈希的正确使用场景
哈希函数在以下场景中是安全的:
- 文件完整性校验:下载文件后验证 SHA-256 哈希
- 数字签名:对文档哈希后签名,而非签名整个文档
- 数据去重:快速判断两个大文件是否相同
- HMAC 消息认证:结合密钥生成消息认证码
二、加盐:让相同的密码产生不同的哈希
2.1 彩虹表攻击
即使使用 SHA-256 这样的安全哈希算法,直接存储密码哈希仍然存在风险。
攻击者可以预先计算大量常见密码的哈希值,建立一个彩虹表(Rainbow Table)。当拿到数据库后,直接查表即可还原密码。
| 密码 | SHA-256 哈希 |
|---|---|
| 123456 | 8d969e... |
| password | 5e8848... |
| qwerty | 65e84b... |
由于哈希的确定性,所有使用 "123456" 的用户,其哈希值完全相同。攻击者只需计算一次,就能破解所有使用该密码的账户。
2.2 什么是盐(Salt)
盐是一个随机生成的字符串,在哈希密码前与密码拼接。每个用户使用不同的盐,即使密码相同,最终的哈希值也完全不同。
import os
import hashlib
import base64
def hash_password_better(password):
# 生成 16 字节的随机盐
salt = os.urandom(16)
# 盐 + 密码 一起哈希
hash_value = hashlib.sha256(salt + password.encode()).digest()
# 存储:盐 + 哈希值
return base64.b64encode(salt + hash_value).decode()
def verify_password(stored, password):
decoded = base64.b64decode(stored.encode())
salt = decoded[:16]
hash_value = decoded[16:]
new_hash = hashlib.sha256(salt + password.encode()).digest()
return new_hash == hash_value2.3 盐的设计原则
- 长度足够:至少 16 字节(128 bit),推荐 32 字节
- 完全随机:使用密码学安全的随机数生成器(CSPRNG)
- 每个用户独立:绝不复用盐,即使密码相同
- 明文存储:盐不需要保密,可以明文存储在数据库中
2.4 盐解决了什么问题,没解决什么
✅ 盐解决了:
- 彩虹表攻击(攻击者需要为每个盐重新计算)
- 相同密码的哈希值相同问题
❌ 盐没解决:
- 哈希速度太快(GPU 仍然可以快速暴力破解单个密码)
- 弱密码本身的安全问题
这就是为什么我们还需要密钥派生函数。
三、密钥派生函数:让哈希计算变慢
3.1 为什么需要慢哈希
密码存储的核心矛盾:
- 合法用户登录:需要快速验证,用户体验要好
- 攻击者暴力破解:需要尽量慢,增加破解成本
3.2 PBKDF2:经典的密钥派生
PBKDF2(Password-Based Key Derivation Function 2)是 RSA 实验室提出的标准算法。
import hashlib
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def hash_password_pbkdf2(password, salt=None):
if salt is None:
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashlib.sha256(),
length=32, # 输出 32 字节
salt=salt,
iterations=600000, # 迭代次数(2023 年 OWASP 推荐)
)
key = kdf.derive(password.encode())
return salt, key关键参数:迭代次数。
- 2010 年:推荐 10,000 次
- 2020 年:推荐 100,000 次
- 2023 年:OWASP 推荐 600,000 次
3.3 bcrypt:自适应的密码哈希
bcrypt 由 Niels Provos 和 David Mazières 在 1999 年设计,是密码存储的经典选择。
import bcrypt
def hash_password_bcrypt(password):
# 自动生成盐,work factor = 12(2^12 轮迭代)
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode(), salt)
return hashed
def verify_password_bcrypt(stored, password):
return bcrypt.checkpw(password.encode(), stored)bcrypt 的优势:
- 自适应:可以通过增加 work factor 随着硬件升级而变"慢"
- 内置盐:无需手动管理盐
- 抗 GPU:基于 Blowfish 密码,使用 4KB 查找表,对 GPU 不太友好
- 广泛支持:几乎所有语言都有成熟库
3.4 scrypt:内存困难的密钥派生
scrypt 由 Colin Percival 设计,不仅计算密集,还内存困难(memory-hard)。
import pyscrypt
def hash_password_scrypt(password):
salt = os.urandom(32)
# N=2^14, r=8, p=1 是标准参数
hash_value = pyscrypt.hash(
password.encode(),
salt,
N=16384, # CPU/内存成本
r=8, # 块大小
p=1, # 并行化
dkLen=32
)
return salt, hash_valuescrypt 的内存困难特性意味着:
- 攻击者无法用 ASIC 或 GPU 大规模并行破解(内存带宽成为瓶颈)
- 合法服务器通常有足够的内存
3.5 Argon2:现代密码哈希之王
Argon2 是 2015 年密码哈希竞赛的获胜者,被推荐为密码存储的首选算法。
Argon2 有三个变体:
- Argon2d:数据依赖型,抗 GPU 攻击最强,但存在侧信道风险
- Argon2i:数据独立型,抗侧信道攻击,推荐用于密码哈希
- Argon2id:混合模式,兼顾两者优势,推荐默认使用
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # 迭代次数
memory_cost=65536, # 64 MB 内存
parallelism=4, # 4 个并行线程
hash_len=32,
salt_len=16
)
def hash_password_argon2(password):
return ph.hash(password)
def verify_password_argon2(stored, password):
try:
ph.verify(stored, password)
return True
except:
return FalseArgon2 的优势:
- 同时控制时间、内存和并行度三个维度
- 抵抗 GPU、ASIC、FPGA 等专用硬件攻击
- 2016 年被推荐为密码哈希的首选算法
- 赢得密码哈希竞赛,经过广泛审查
四、算法对比与选择建议
4.1 各算法特性对比
| 特性 | bcrypt | scrypt | Argon2id | PBKDF2 |
|---|---|---|---|---|
| 自适应 | ✅ | ✅ | ✅ | ✅(手动调参数) |
| 内存困难 | ❌ | ✅ | ✅ | ❌ |
| 抗 GPU | 中等 | 强 | 最强 | 弱 |
| 抗 ASIC | 弱 | 中等 | 强 | 弱 |
| 侧信道安全 | ✅ | ✅ | ✅ (Argon2i/id) | ✅ |
| 标准支持 | 广泛 | 较广 | 增长中 | 最广泛 |
4.2 选择建议
新项目(2024+):
- 首选 Argon2id,参数:time_cost=3, memory_cost=64MB, parallelism=4
- 如果库支持有限,使用 bcrypt(cost=12+)
现有系统升级:
- 如果正在用 MD5/SHA1/SHA256 + 盐:立即迁移到 bcrypt/Argon2
- 迁移策略:用户下次登录时,用新算法重新哈希并替换旧值
def migrate_password(user, password):
if user.hash_type == 'legacy':
# 验证旧哈希
if verify_legacy(user.stored_hash, password):
# 用新算法重新哈希
user.new_hash = hash_password_argon2(password)
user.hash_type = 'argon2id'
user.save()
return True
return verify_argon2(user.new_hash, password)五、常见错误与最佳实践
5.1 ❌ 错误示范
# 错误 1:无盐哈希
hashlib.sha256(password.encode()).hexdigest()
# 错误 2:使用快速哈希
hashlib.md5(salt + password).hexdigest()
# 错误 3:盐太短或可预测
salt = username[:8] # 可预测!
# 错误 4:自定义哈希组合
sha256(md5(password) + salt) # 不增加安全性,反而可能引入漏洞
# 错误 5:硬编码盐
SALT = "myapp_salt_2024" # 所有用户共享!5.2 ✅ 最佳实践
- 永远不要自己实现密码哈希算法,使用经过审查的库
- 默认使用 Argon2id,其次 bcrypt
- 参数要足够强:随着硬件升级定期调整
- 盐必须随机且独立:每个用户、每次改密码都重新生成
- 添加 pepper(可选):一个全局密钥存储在代码/环境变量中,即使数据库泄露也增加破解难度
# pepper 的使用(可选增强)
PEPPER = os.environ.get('PASSWORD_PEPPER') # 32 字节随机密钥
def hash_with_pepper(password, salt):
# pepper 不存储在数据库,而是放在配置/密钥管理系统
return argon2.hash(password + PEPPER, salt)- 实施登录速率限制:即使哈希很强,也要防止在线暴力破解
- 监控异常登录:同一 IP 大量失败尝试时告警/封禁
六、总结
密码存储不是"哈希一下"那么简单,而是一个需要精心设计的系统工程:
| 层次 | 措施 | 目的 |
|---|---|---|
| 算法选择 | Argon2id / bcrypt | 让哈希计算变慢 |
| 盐 | 每个用户独立随机 | 防御彩虹表 |
| Pepper | 全局密钥(可选) | 数据库泄露后的额外保护 |
| 速率限制 | 登录失败限制 | 防御在线暴力破解 |
| 监控 | 异常登录告警 | 及时发现攻击 |
密码学没有"差不多安全",只有"安全"和"不安全"。当你犹豫用 MD5 还是 bcrypt 时,选择 bcrypt;当你犹豫 bcrypt 的 cost 用 10 还是 12 时,选择 12。在密码存储这件事上,过度谨慎永远不是错误。
系列索引:
- SQL注入攻击与防御
- XSS跨站脚本攻击
- JWT安全:令牌伪造、篡改与防御
- Session管理安全
- 密码学基础:哈希、加盐与密钥派生 ← 本文
- 下一篇:多因素认证MFA:绕过与防御
本文属于「网络安全系列」认证与授权篇。如有疑问或补充,欢迎在评论区留言讨论。