不能直接用MD5或SHA256哈希密码,因其无盐且计算快,易受彩虹表和GPU暴力破解;应使用带随机盐、高迭代次数的慢哈希算法如PBKDF2(Rfc2898DeriveBytes),盐长≥32字节,迭代≥100000次,并用FixedTimeEquals安全比对。

为什么不能直接用 MD5 或 SHA256 哈希密码
因为这些是纯哈希函数,没有加盐机制,攻击者可以用彩虹表或 GPU 暴力预计算快速反查。哪怕你用了 SHA256,只要用户密码是 "123456",哈希值就永远固定,毫无安全性可言。
真正安全的做法是:每次生成一个随机盐(salt),和密码拼接后再用慢哈希算法处理。.NET 从 4.7.2 起内置了推荐方案——Rfc2898DeriveBytes(即 PBKDF2),它自带盐、可调迭代次数、抗 GPU 加速。
- 盐必须每次生成新的,长度 ≥ 16 字节(推荐 32)
- 迭代次数建议 ≥ 100_000(.NET 默认 10_000,太低)
- 输出密钥长度应为 32 字节(对应 SHA256)或 64 字节(对应 SHA512)
用 Rfc2898DeriveBytes 正确加盐哈希密码
别自己拼接盐和密码再喂给 SHA256.Create()——那不是加盐哈希,只是“带盐的普通哈希”,盐没绑定到结果里,且没抗暴力设计。
正确做法是让 Rfc2898DeriveBytes 内部管理盐和迭代:
public static (byte[] hash, byte[] salt) HashPassword(string password)
{
var salt = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256);
return (pbkdf2.GetBytes(32), salt);
}
注意:Rfc2898DeriveBytes 构造时传入的 salt 是字节数组,不是字符串;它内部会做 PKCS#5 兼容处理,无需手动 Base64 或 Hex 编码盐。
- 存储时要把
hash和salt一起存(比如 JSON 字段{"hash": "base64...", "salt": "base64..."}) - 验证时用同样的盐和迭代次数重新算一次,比对
hash字节数组是否相等(用CryptographicOperations.FixedTimeEquals,别用==) - 别把迭代次数硬编码在逻辑里——它属于密码策略,应配置化(如从
IConfiguration读取)
为什么不用 BCrypt.Net-Next 或 Microsoft.AspNetCore.Cryptography.KeyDerivation
BCrypt.Net-Next 是成熟选择,支持自适应成本因子,但需额外 NuGet 包;而 Microsoft.AspNetCore.Cryptography.KeyDerivation(含 Pbkdf2 和 Spa)本质也是封装 Rfc2898DeriveBytes,API 更现代但底层相同。
如果你项目已引用 Microsoft.AspNetCore.App,用后者写法更简洁:
var hash = KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100_000,
numBytesRequested: 32);
但要注意:KeyDerivation.Pbkdf2 不返回盐,盐还得自己生成并保管;它也不校验输入长度,若 salt 太短(如 8 字节),安全性会下降。
-
Rfc2898DeriveBytes更底层可控,适合需要明确掌控流程的场景 -
KeyDerivation.Pbkdf2更轻量,适合 ASP.NET Core Web API 快速集成 - 两者都不支持 Argon2——如需更高内存抗性,得引入
libsodium-net或Argon2Net
验证密码时最容易忽略的三个细节
哈希存好了,验证却常出错,问题多出在“怎么比”上:
- 盐必须原样复用,不能重新生成——否则算出来的 hash 永远不匹配
- 比对 hash 时用
CryptographicOperations.FixedTimeEquals(hash1, hash2),防止时序攻击;用SequenceEqual或==在某些情况下会提前退出,暴露字节差异 - 密码字符串要先
Encoding.UTF8.GetBytes()再喂给哈希器,避免 Unicode 归一化或 BOM 导致意外差异(例如用户复制粘贴时带不可见字符)
还有个隐形坑:数据库字段类型。如果把 hash 存成 nvarchar,又用 UTF-16 解码再哈希,结果必然错。务必统一用 varbinary 或 Base64 字符串(varchar)存二进制数据。


















