C#密码安全存储:从MD5哈希、加盐到PBKDF2/BCrypt的演进与实践
2026/7/13 5:14:08 网站建设 项目流程

1. 项目概述:为什么我们还在用MD5加密密码?

在C#开发中,尤其是涉及到用户登录、注册功能的Web应用、桌面软件或者移动端后台,密码的安全存储是一个绕不开的基石问题。你可能经常看到类似“密码加密后存储”的要求,而MD5往往是第一个被提及的方案。这个项目标题“C# 基于MD5实现密码加密功能,附源码”直指一个非常经典且实用的场景。但作为有经验的开发者,我们必须清醒地认识到:在今天,单纯地使用MD5进行密码加密,已经是一种过时且存在安全隐患的做法。那为什么还要讨论它?因为理解MD5,是理解现代密码安全体系的起点。很多遗留系统仍在用它,更重要的是,通过实现它,我们能透彻理解“哈希”、“盐值”、“彩虹表”这些安全核心概念,为后续采用更安全的方案(如PBKDF2、BCrypt)打下坚实基础。

简单来说,这个项目就是教你如何在C#中,使用System.Security.Cryptography命名空间下的MD5类,将用户输入的明文密码转换为一串固定长度的、看似随机的哈希值(Hash),并将这串哈希值存储到数据库中。当用户再次登录时,用同样的算法处理其输入的密码,比较两次生成的哈希值是否一致。整个过程是单向的,理论上无法从哈希值反推出原始密码。接下来,我会带你从最基础的实现开始,逐步深入到安全性增强、代码优化以及在现代开发中的正确实践。

2. 核心原理与安全性深度剖析

2.1 MD5算法本质:它到底是什么?

MD5(Message-Digest Algorithm 5)是一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值。在C#中,我们最终看到的通常是这个128位值转换成的32位十六进制字符串(每个字节用两个十六进制字符表示,所以是32位)。

它的核心特性是:

  1. 单向性:从输入到输出容易,但从输出反推输入在计算上不可行。这是它被用于密码“加密”(更准确说是“哈希”或“摘要”)的理论基础。
  2. 雪崩效应:输入密码哪怕只改变一个字符(例如从“password”改为“passworD”),产生的MD5哈希值也会发生巨大、看似无规律的变化。
  3. 固定长度:无论输入是“1”还是一整本书,输出的MD5哈希值永远是128位。

在C#中,我们通过MD5.Create()方法获取MD5算法的实例,然后调用ComputeHash(byte[] buffer)方法来完成计算。这个过程本身不涉及“密钥”,是一个确定性的公开算法。

2.2 为什么“单纯MD5”已经不安全?

这是本项目的关键认知升级点。虽然MD5算法本身是单向的,但它在密码存储场景下存在几个致命缺陷:

  1. 彩虹表攻击:由于算法公开且确定,攻击者可以预先计算海量常用密码及其对应的MD5哈希值,做成一个庞大的“密码-哈希值”映射表,这就是彩虹表。当攻击者拿到数据库泄露的MD5哈希值时,只需在这个表里一查,就能立刻得到对应的原始密码。网上有大量在线的MD5解密网站,其原理就是背后有一个巨大的彩虹表数据库。

  2. 碰撞漏洞:从密码学角度,MD5的抗碰撞性已被攻破。这意味着攻击者可以找到两个不同的输入,产生相同的MD5哈希值。虽然直接用于伪造特定密码有难度,但这严重动摇了其作为安全基石的可信度。

  3. 速度过快:现代计算机和GPU可以每秒进行数十亿次MD5计算。这使得暴力破解(尝试所有可能组合)和彩虹表攻击的效率极高。

重要提示:因此,在任何新的、对安全有要求的项目中,绝对不应该直接使用明文MD5哈希来存储密码。我们接下来的实现,重点在于理解原理和掌握向更安全方案过渡的方法。

2.3 安全性增强的核心:加盐

为了抵御彩虹表攻击,“加盐”是必须的步骤。盐是一个随机生成的、足够长的字符串(或字节数组)。安全流程如下:

  1. 为用户注册时,系统生成一个唯一的随机盐值。
  2. 将用户密码和这个盐值拼接起来(例如password + saltsalt + password),然后对拼接后的字符串计算MD5哈希。
  3. 将计算得到的哈希值盐值一起存储到数据库的用户记录中。

验证时:

  1. 从数据库取出该用户的哈希值和盐值。
  2. 将用户输入的登录密码与取出的盐值拼接,计算MD5。
  3. 比较计算出的哈希值与数据库中存储的哈希值是否一致。

加盐为什么有效?彩虹表是针对“密码->哈希”的映射。加盐后,实际哈希的是“密码+随机盐”。这个随机盐使得预计算的彩虹表完全失效,因为攻击者不可能为每一个可能的盐值都预计算一个彩虹表(那将需要天文数字般的存储空间)。攻击者必须针对每个用户单独进行暴力破解,成本和时间呈指数级增长。

3. C#实现MD5哈希与加盐的完整代码解析

下面,我将提供一个生产环境中更健壮、更安全的实现示例,它包含了加盐、多次迭代等增强措施,并解释了每一步的考量。

3.1 基础工具类:MD5哈希生成

首先,我们创建一个静态工具类,提供核心的哈希方法。注意,这里我们直接使用.NET框架推荐的MD5.Create(),而不是已过时的MD5CryptoServiceProvider

using System.Security.Cryptography; using System.Text; namespace PasswordSecurity.Utils { /// <summary> /// 密码哈希工具类(演示用途,生产环境建议使用更安全的算法如PBKDF2或BCrypt) /// </summary> public static class PasswordHasher { /// <summary> /// 为密码生成一个随机的盐值 /// </summary> /// <param name="size">盐值的字节长度,推荐16字节(128位)或以上</param> /// <returns>Base64编码的盐值字符串</returns> public static string GenerateSalt(int size = 16) { // 使用密码学安全的随机数生成器 using (var rng = RandomNumberGenerator.Create()) { byte[] saltBytes = new byte[size]; rng.GetBytes(saltBytes); // 用随机字节填充数组 return Convert.ToBase64String(saltBytes); } } /// <summary> /// 使用MD5和盐值对密码进行哈希 /// </summary> /// <param name="password">明文密码</param> /// <param name="salt">盐值(Base64字符串)</param> /// <param name="iterations">迭代次数,增加破解成本(默认为1,MD5本身不支持内置迭代,这里模拟流程)</param> /// <returns>Base64编码的哈希值字符串</returns> public static string HashPasswordWithMD5(string password, string salt, int iterations = 1) { if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("密码不能为空", nameof(password)); if (string.IsNullOrWhiteSpace(salt)) throw new ArgumentException("盐值不能为空", nameof(salt)); byte[] saltBytes = Convert.FromBase64String(salt); byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // 将密码和盐拼接 byte[] saltedPassword = new byte[passwordBytes.Length + saltBytes.Length]; Buffer.BlockCopy(passwordBytes, 0, saltedPassword, 0, passwordBytes.Length); Buffer.BlockCopy(saltBytes, 0, saltedPassword, passwordBytes.Length, saltBytes.Length); byte[] hashBytes = saltedPassword; using (var md5 = MD5.Create()) { // 模拟迭代:多次哈希以增加计算成本(实际MD5.Create()每次都是新的,这里仅为演示概念) for (int i = 0; i < iterations; i++) { // 注意:简单串联迭代对MD5安全性提升有限,专业算法如PBKDF2有更科学的迭代机制。 hashBytes = md5.ComputeHash(hashBytes); } } return Convert.ToBase64String(hashBytes); } /// <summary> /// 验证密码是否匹配 /// </summary> /// <param name="password">待验证的明文密码</param> /// <param name="storedHash">数据库中存储的Base64哈希值</param> /// <param name="storedSalt">数据库中存储的Base64盐值</param> /// <param name="iterations">哈希时使用的迭代次数,必须与创建时一致</param> /// <returns>验证通过返回true,否则返回false</returns> public static bool VerifyPassword(string password, string storedHash, string storedSalt, int iterations = 1) { string computedHash = HashPasswordWithMD5(password, storedSalt, iterations); // 使用固定时间比较算法,防止时序攻击(简单场景下直接比较也可,此处展示最佳实践) return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(computedHash), Encoding.UTF8.GetBytes(storedHash) ); } } }

代码关键点解析:

  1. GenerateSalt方法:使用RandomNumberGenerator.Create()而非Random类来生成盐。Random是伪随机,可能被预测,而RandomNumberGenerator是密码学安全的随机数生成器(CSPRNG),能产生高质量的随机数,这是安全盐的基石。
  2. 盐的存储:盐不需要保密,但必须唯一且随机。我们将其转换为Base64字符串,便于存储在数据库的VARCHARTEXT字段中。
  3. 拼接方式:代码中使用了Buffer.BlockCopy将密码字节数组和盐字节数组拼接。顺序(密码在前还是盐在前)需要固定,验证时必须使用相同的顺序。
  4. 迭代次数:参数iterations用于模拟“密钥延伸”概念。通过多次哈希,故意增加计算一个哈希值所需的时间,从而大幅提高暴力破解的成本。对于MD5,这只是概念演示,因为每次MD5.Create()都是新实例。在生产中,应使用原生支持迭代的算法如Rfc2898DeriveBytes(PBKDF2)。
  5. 验证方法VerifyPassword方法重新计算哈希并进行比较。注意使用了CryptographicOperations.FixedTimeEquals方法进行比较。普通的字符串比较(==.Equals)在发现第一个不匹配的字符时会立即返回,这可能导致“时序攻击”——攻击者通过测量验证耗时,可以逐步推测出正确的哈希值。固定时间比较算法确保无论匹配与否,比较耗时都基本相同,堵住这个细微但真实存在的安全漏洞。

3.2 用户注册与登录的完整流程示例

假设我们有一个简单的用户实体和数据库上下文。

// 用户实体类 public class User { public int Id { get; set; } public string Username { get; set; } public string PasswordHash { get; set; } // 存储哈希值 public string PasswordSalt { get; set; } // 存储盐值 public int HashIterations { get; set; } = 1; // 存储迭代次数,为未来升级算法留字段 } // 用户服务类 public class UserService { public (bool Success, string Message, User User) Register(string username, string password) { // 1. 检查用户名是否存在(略) // 2. 生成盐 string salt = PasswordHasher.GenerateSalt(); // 3. 设置迭代次数(此处为演示,实际MD5迭代意义不大) int iterations = 1000; // 假设我们迭代1000次 // 4. 计算密码哈希 string hash = PasswordHasher.HashPasswordWithMD5(password, salt, iterations); // 5. 创建用户对象并保存(这里模拟保存) var newUser = new User { Username = username, PasswordHash = hash, PasswordSalt = salt, HashIterations = iterations }; // _dbContext.Users.Add(newUser); // _dbContext.SaveChanges(); Console.WriteLine($"用户注册成功!用户名:{username}, 盐:{salt}, 哈希:{hash}"); return (true, "注册成功", newUser); } public (bool Success, string Message, User User) Login(string username, string password) { // 1. 根据用户名从数据库获取用户(模拟) // var user = _dbContext.Users.FirstOrDefault(u => u.Username == username); User user = GetUserFromDatabase(username); // 假设这个方法能返回用户 if (user == null) { return (false, "用户名或密码错误", null); // 统一提示,避免暴露用户是否存在 } // 2. 使用存储的盐和迭代次数验证密码 bool isPasswordCorrect = PasswordHasher.VerifyPassword(password, user.PasswordHash, user.PasswordSalt, user.HashIterations); if (isPasswordCorrect) { return (true, "登录成功", user); } else { return (false, "用户名或密码错误", null); } } private User GetUserFromDatabase(string username) { // 模拟数据库查询,返回一个测试用户 // 实际应从数据库读取 return new User { Id = 1, Username = "testuser", PasswordHash = "某个之前计算好的哈希值", // 这里需要替换为真实哈希 PasswordSalt = "某个之前生成的盐值", // 这里需要替换为真实盐 HashIterations = 1000 }; } }

流程要点:

  • 注册盐生成 -> 计算哈希 -> 存储(用户名、哈希、盐、迭代次数)
  • 登录根据用户名取回用户记录(含盐和哈希)-> 用输入密码和存储的盐计算哈希 -> 比较哈希值
  • 安全提示:登录失败时,返回统一的错误信息“用户名或密码错误”,而不要提示“用户不存在”或“密码错误”。这可以防止攻击者通过反馈信息枚举出有效的用户名。

4. 从MD5升级到现代密码哈希方案

理解了MD5加盐的原理后,我们必须向前看。.NET提供了远比手动MD5加盐更安全、更专业的API。

4.1 使用PBKDF2(Rfc2898DeriveBytes)

PBKDF2(Password-Based Key Derivation Function 2)是当前推荐的标准之一,它内置了加盐和多次迭代的概念。

using System.Security.Cryptography; public static class SecurePasswordHasher { // 定义哈希算法、迭代次数和输出长度 private const int SaltSize = 16; // 128位盐 private const int HashSize = 32; // 256位哈希输出(SHA256) private const int Iterations = 100000; // 迭代次数,应根据硬件性能调整(2024年推荐10万次以上) /// <summary> /// 创建密码哈希 /// </summary> public static string Hash(string password) { // 生成密码学安全的随机盐 using (var rng = RandomNumberGenerator.Create()) { byte[] salt = new byte[SaltSize]; rng.GetBytes(salt); // 使用PBKDF2派生密钥 using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256)) { byte[] hash = pbkdf2.GetBytes(HashSize); // 组合格式:迭代次数:盐:哈希 return $"{Iterations}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; } } } /// <summary> /// 验证密码 /// </summary> public static bool Verify(string password, string hashedPassword) { // 解析存储的字符串 var parts = hashedPassword.Split(':'); if (parts.Length != 3) throw new FormatException("哈希密码格式不正确"); int iterations = int.Parse(parts[0]); byte[] salt = Convert.FromBase64String(parts[1]); byte[] storedHash = Convert.FromBase64String(parts[2]); // 使用相同的参数计算输入密码的哈希 using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256)) { byte[] computedHash = pbkdf2.GetBytes(storedHash.Length); // 固定时间比较 return CryptographicOperations.FixedTimeEquals(computedHash, storedHash); } } }

PBKDF2的优势:

  • 标准化:是NIST等标准机构认可的算法。
  • 可配置迭代次数:可以通过增加迭代次数来对抗计算能力的提升,而无需修改算法。
  • 内置盐管理Rfc2898DeriveBytes类简化了流程。
  • 更安全的哈希函数:默认使用SHA-1,但推荐指定更安全的SHA-256或SHA-512。

4.2 使用BCrypt(通过第三方库)

BCrypt是专门为密码哈希设计的算法,它自动处理盐的生成和嵌入,并且输出字符串包含了算法版本、成本因子(迭代次数的对数)、盐和哈希值,全部整合在一个字符串里,使用非常方便。

你需要通过NuGet安装类似BCrypt.Net-Next的库。

using BCrypt.Net; // 需要安装 BCrypt.Net-Next 包 public static class BcryptPasswordHasher { // 工作因子(Work Factor)控制计算成本,每增加1,耗时约翻倍。推荐值10-12。 private const int WorkFactor = 11; public static string Hash(string password) { // 自动生成盐并返回包含所有信息的哈希字符串 return BCrypt.HashPassword(password, WorkFactor); } public static bool Verify(string password, string hashedPassword) { // 直接从哈希字符串中提取盐并进行验证 return BCrypt.Verify(password, hashedPassword); } }

BCrypt的优势:

  • 一体化:哈希字符串自包含所有信息(算法、盐、成本因子),存储和验证极其简单。
  • 自适应慢哈希:算法本身设计得较慢,且成本因子可调,能有效抵抗硬件破解。
  • 广泛认可:在安全社区中被广泛认为是存储密码的最佳实践之一。

5. 数据库设计与存储实践

无论使用哪种哈希方案,数据库表的设计都至关重要。

CREATE TABLE Users ( Id INT PRIMARY KEY IDENTITY(1,1), Username NVARCHAR(100) NOT NULL UNIQUE, -- 方案1:使用MD5/PBKDF2,需分开存储 PasswordHash NVARCHAR(500) NOT NULL, -- 存储Base64编码的哈希值 PasswordSalt NVARCHAR(500) NOT NULL, -- 存储Base64编码的盐值 HashAlgorithm NVARCHAR(50) NOT NULL DEFAULT 'PBKDF2-SHA256', -- 记录使用的算法,便于未来升级 HashIterations INT NOT NULL DEFAULT 100000, -- 记录迭代次数 -- 方案2:使用BCrypt,只需一个字段 -- PasswordHash NVARCHAR(500) NOT NULL, -- 存储BCrypt的完整哈希字符串 CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(), LastLoginAt DATETIME2 NULL );

字段设计说明:

  • PasswordHash:存储最终的哈希结果。如果使用BCrypt,这个字段就存储其输出的完整字符串(如$2a$11$r9xSZh...)。如果使用PBKDF2或自定义方案,可以存储Base64编码的哈希字节。
  • PasswordSalt:如果方案需要单独存盐(如PBKDF2或我们自定义的MD5),则需要此字段。BCrypt不需要。
  • HashAlgorithmHashIterations:这是非常重要的元数据字段。它们记录了密码是用什么算法和参数哈希的。当未来你需要升级哈希算法(比如从MD5迁移到PBKDF2)时,可以在用户下次成功登录时,用新算法重新哈希其密码并更新这些字段。这实现了算法的无缝升级。
  • 长度NVARCHAR(500)提供了足够的空间,即使对于BCrypt的长输出字符串也绰绰有余。

6. 常见问题、陷阱与实战经验

6.1 编码问题:为什么我的MD5哈希和别人算的不一样?

这是新手最常见的坑。MD5计算的是字节数组的哈希,而不是字符串的哈希。字符串转换为字节数组需要编码。

string password = "hello世界"; byte[] bytes1 = Encoding.UTF8.GetBytes(password); // 使用UTF-8编码 byte[] bytes2 = Encoding.Default.GetBytes(password); // 使用系统默认编码(在中文Windows上是GBK) byte[] bytes3 = Encoding.ASCII.GetBytes(password); // ASCII无法编码中文,会丢失信息 // bytes1, bytes2, bytes3 是不同的数组,计算出的MD5也完全不同。

黄金法则:在整个应用的生命周期中,必须使用同一种编码。强烈推荐使用Encoding.UTF8,因为它是跨平台、跨语言的标准。在HashPasswordWithMD5VerifyPassword方法中,必须使用相同的编码(我们在工具类中固定使用了Encoding.UTF8)。

6.2 哈希输出格式:十六进制还是Base64?

MD5的128位输出(16字节)通常表示为32位的十六进制字符串(每个字节变成两个十六进制字符)。但也可以转换为Base64字符串。

  • 十六进制:可读性好,BitConverter.ToString(hashBytes).Replace("-", "")即可得到。长度固定32字符。
  • Base64:更紧凑,Convert.ToBase64String(hashBytes),长度约为24字符。更适合存储在数据库或传输中。

选择建议:在代码内部处理时,使用字节数组byte[]最直接。需要存储或传输时,转换为Base64。如果为了调试或与某些旧系统兼容,可以输出十六进制。在我们的工具类中,为了存储方便,统一使用了Base64。

6.3 关于“解密MD5”的误解

必须再次强调:MD5是单向哈希函数,不是加密算法。加密(如AES、DES)是可逆的,有密钥即可解密。哈希是不可逆的。网上所谓的“MD5解密”网站,实质是彩虹表查询服务。如果你的密码是“123456”,它的MD5哈希是公开的,当然能“解”出来。但如果你的密码是“MySuperStrongPassw0rd!2024”,并且加了随机盐,那么任何网站都无法“解密”。

6.4 性能考量:迭代次数设置多少合适?

对于PBKDF2,迭代次数的目标是使得在你的服务器上计算一次哈希需要大约100到500毫秒。这个延迟对用户登录体验影响微乎其微(一次登录只验证一次),但足以让攻击者进行大规模暴力破解的速度变得无法接受。

你可以写一个简单的基准测试程序来调整迭代次数:

public static int FindOptimalIterationTime() { string testPassword = "TestPassword123!"; byte[] salt = new byte[16]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt); int iterations = 10000; var stopwatch = new System.Diagnostics.Stopwatch(); while (true) { stopwatch.Restart(); using (var pbkdf2 = new Rfc2898DeriveBytes(testPassword, salt, iterations, HashAlgorithmName.SHA256)) { _ = pbkdf2.GetBytes(32); } stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds >= 100) // 目标100ms { Console.WriteLine($"达到100ms的迭代次数约为: {iterations}"); return iterations; } iterations += 10000; // 增加迭代次数 } }

在2024年,主流服务器的CPU上,PBKDF2-SHA256的迭代次数通常在10万到50万次之间才能达到100-500ms。这个值需要定期(例如每两年)重新评估并增加。

6.5 密码策略与前端传输

  1. 密码复杂度:强制要求用户使用足够强度的密码(长度、大小写字母、数字、符号)。但这只是第一道防线。
  2. 前端传输永远不要在前端用JavaScript计算密码哈希然后传输。这会让哈希值成为事实上的“密码”,如果传输过程不是HTTPS,哈希值被截获同样可以用于登录(这称为“哈希传递”攻击)。正确的做法是前端通过HTTPS传输明文密码到后端,由后端进行哈希处理。HTTPS保证了传输过程的安全。
  3. 密码散列:对于特别敏感的系统,可以考虑在客户端先进行一次哈希(使用固定盐或方案),然后在服务端再次加盐哈希。这可以防止原始密码明文在服务端内存中短暂出现(尽管在HTTPS下风险已很低),并能在服务端数据库泄露时提供额外保护。但这增加了复杂性,需谨慎设计。

7. 完整可运行的控制台示例与测试

最后,我们用一个完整的控制台程序来演示整个流程,从注册到登录验证。

using System; using System.Security.Cryptography; using System.Text; namespace MD5PasswordDemo { class Program { static void Main(string[] args) { Console.WriteLine("=== C# 密码哈希与验证演示 ===\n"); // 模拟用户注册 string username = "zhangsan"; string rawPassword = "MySecurePassw0rd!"; Console.WriteLine($"用户注册:"); Console.WriteLine($" 用户名: {username}"); Console.WriteLine($" 明文密码: {rawPassword}"); // 1. 生成盐 string salt = GenerateSalt(); Console.WriteLine($" 生成盐: {salt}"); // 2. 使用MD5加盐哈希(演示用,不推荐生产) string md5Hash = HashWithMD5AndSalt(rawPassword, salt); Console.WriteLine($" MD5加盐哈希: {md5Hash}"); // 3. 使用PBKDF2哈希(推荐) string pbkdf2Hash = HashWithPBKDF2(rawPassword); Console.WriteLine($" PBKDF2哈希: {pbkdf2Hash}"); Console.WriteLine("\n--- 模拟数据库存储 ---"); Console.WriteLine($" 存储的用户记录:"); Console.WriteLine($" Username: {username}"); Console.WriteLine($" Salt (for MD5): {salt}"); Console.WriteLine($" MD5_Hash: {md5Hash}"); Console.WriteLine($" PBKDF2_Hash: {pbkdf2Hash}"); // 模拟用户登录 Console.WriteLine("\n=== 用户登录验证 ===\n"); TestLogin(username, "MySecurePassw0rd!", salt, md5Hash, pbkdf2Hash); // 正确密码 TestLogin(username, "WrongPassword", salt, md5Hash, pbkdf2Hash); // 错误密码 Console.WriteLine("\n演示结束。"); } static string GenerateSalt(int size = 16) { using (var rng = RandomNumberGenerator.Create()) { byte[] saltBytes = new byte[size]; rng.GetBytes(saltBytes); return Convert.ToBase64String(saltBytes); } } static string HashWithMD5AndSalt(string password, string salt) { byte[] saltBytes = Convert.FromBase64String(salt); byte[] passwordBytes = Encoding.UTF8.GetBytes(password); byte[] combined = new byte[passwordBytes.Length + saltBytes.Length]; Buffer.BlockCopy(passwordBytes, 0, combined, 0, passwordBytes.Length); Buffer.BlockCopy(saltBytes, 0, combined, passwordBytes.Length, saltBytes.Length); using (var md5 = MD5.Create()) { byte[] hashBytes = md5.ComputeHash(combined); return Convert.ToBase64String(hashBytes); } } static bool VerifyMD5Hash(string password, string salt, string storedHash) { string computedHash = HashWithMD5AndSalt(password, salt); // 简单比较,生产环境应用FixedTimeEquals return computedHash == storedHash; } static string HashWithPBKDF2(string password, int iterations = 100000) { byte[] salt = new byte[16]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt); using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256)) { byte[] hash = pbkdf2.GetBytes(32); // 256-bit output return $"{iterations}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; } } static bool VerifyPBKDF2Hash(string password, string hashedPassword) { var parts = hashedPassword.Split(':'); if (parts.Length != 3) return false; int iterations = int.Parse(parts[0]); byte[] salt = Convert.FromBase64String(parts[1]); byte[] storedHash = Convert.FromBase64String(parts[2]); using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256)) { byte[] computedHash = pbkdf2.GetBytes(storedHash.Length); return CryptographicOperations.FixedTimeEquals(computedHash, storedHash); } } static void TestLogin(string username, string inputPassword, string storedSalt, string storedMD5Hash, string storedPBKDF2Hash) { Console.WriteLine($"\n 尝试登录用户 '{username}',输入密码: '{inputPassword}'"); bool md5Valid = VerifyMD5Hash(inputPassword, storedSalt, storedMD5Hash); bool pbkdf2Valid = VerifyPBKDF2Hash(inputPassword, storedPBKDF2Hash); Console.WriteLine($" MD5验证结果: {(md5Valid ? "成功" : "失败")}"); Console.WriteLine($" PBKDF2验证结果: {(pbkdf2Valid ? "成功" : "失败")}"); } } }

运行这个程序,你会看到完整的流程输出,直观地理解盐的生成、哈希的计算以及验证过程。通过这个项目,你不仅学会了如何在C#中使用MD5,更重要的是理解了密码安全存储的核心原则,并掌握了向PBKDF2等更安全方案迁移的路径。记住,在安全问题上,使用经过充分验证的、标准的库和算法,永远比自己从头实现更可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询