理解和实现密码哈希和加盐的初学者教程






4.87/5 (33投票s)
在本文中,我们将讨论如何通过对用户密码使用哈希和加盐(salting)来保护用户密码。
引言
在本文中,我们将讨论如何通过对用户密码使用哈希和加盐(salting)来保护用户密码。我们将首先讨论哈希和加盐的理论。然后,我们将尝试实现一个可以用于执行哈希和加盐的简单库。
背景
我见过许多 ASP.NET 新手犯的一个错误,就是将密码明文存储在数据库中。我问他们的第一个问题是,为什么不使用 ASP.NET 的 Roles 和 Membership 数据库和 API。在大多数情况下,使用默认的 Roles 和 Membership API 可以满足应用程序的需求。在某些情况下,应用程序确实需要拥有自己的表来保存用户凭据。所有使用自定义表单身份验证的应用程序很可能在应用程序的特定架构中将用户凭据存储在应用程序数据库中。
注意:请参考这两篇文章,了解更多关于自定义表单身份验证的内容
明文密码
对于所有需要将用户凭据存储在应用程序表中的场景,将密码明文存储绝不是一个好主意。任何能够访问数据库的人都可以轻易地得知所有用户的密码。此外,即使应用程序中一个容易受到 SQL 注入攻击的小部分,也可能泄露所有用户的密码。那么,如何才能防止这种情况并以更好的方式存储密码呢?
存储加密密码
加密是将消息编码,以便即使有人获取了消息,也无法从中解释任何内容。要加密消息,我们需要两样东西。首先,我们需要一个加密算法;其次,我们需要一个用于加密的密钥。
因此,如果要求不是明文存储密码,那么第一个想法就是将密码加密而不是明文存储。这种方法比明文存储密码要好,但仍然存在一些问题。如果有人知道用于加密的加密算法和密钥,他们就可以轻松地解密密码。
哈希 - 存储密码哈希值
哈希是为较长的字符串消息生成一个数字或唯一字符串的过程。每个字符串消息的哈希值都应该是唯一的,并且无法从其哈希值中重现原始消息。
无论我们的加密机制有多强大,只要知道算法和密钥,总有可能重新生成原始密码。因此,更好的方法是将密码哈希值存储在表中。这样,就无法从哈希值中重现密码。每当用户尝试登录时,我们将使用相同的哈希算法为密码生成哈希值,然后将其与数据库中存储的哈希值进行比较,以检查密码是否正确。
现在,这种方法比存储加密密码要好得多。但它也有一些局限性。为了理解这些局限性,让我们看下面的例子:
用户 | 密码 | 哈希 |
user1 | one | 1234 |
user2 | two | 2345 |
user3 | three | 3456 |
user4 | one | 1234 |
user5 | two | 2345 |
上表显示了一些虚拟数据,其中 5 位用户选择了他们的密码,并生成了相应的哈希值存储在数据库中。现在,从这张表中我们可以清楚地看到存储哈希密码的局限性。问题在于,
上表显示了一些虚拟数据,其中 5 位用户选择了他们的密码,并生成了相应的哈希值存储在数据库中。现在,从这张表中我们可以清楚地看到存储哈希密码的局限性。问题在于,`user1` 和 `user4` 选择的密码相同,因此生成的密码哈希值也相同。`user2` 和 `user5` 的情况也是如此。因此,如果我获取了这些密码哈希值,并且我知道 `user1` 的密码,我实际上就知道了所有密码哈希值与 `user1` 密码哈希值相同的用户的密码。
现在,这个问题比前面方法的问题要轻微。但我们能否设计一种技术,它能够存储并提供哈希的所有好处,同时又能消除与之相关的局限性?答案是:加盐(salting)和哈希(hashing)。
密码的加盐和哈希
加盐是一种技术,在这种技术中,我们在用户输入的密码中添加一个随机字符串,然后对生成的字符串进行哈希。因此,即使两个人选择了相同的密码,他们的盐值也会不同。所以,即使他们选择了相同的密码,两个用户的哈希值也永远不会相同。下表将更详细地说明这个概念。
用户 | 密码 | 随机盐 | 结果密码字符串 | 哈希 |
user1 | one | 12 | one12 | 1234 |
user2 | two | 23 | two23 | 2345 |
user3 | three | 34 | three34 | 3456 |
user4 | one | 45 | one45 | 4567 |
user5 | two | 56 | two56 | 5678 |
现在,如果我们看上面的表,我们可以看到,即使 `user1` 和 `user4` 选择的密码相同,他们的盐值也不同,因此结果哈希值也不同。现在,在这个方法中要注意的重要一点是,盐值应该始终是随机的,其次,它也应该存储在数据库中,以便在比较用户输入的密码时可以使用。
使用代码
现在我们已经看到了密码哈希和加盐相关的理论,以及为什么这种技术比存储明文密码、加密密码甚至存储密码哈希值更受青睐。现在,让我们看看如何在 .NET 中实现这种加盐和哈希,以便像 ASP.NET 或 ASP.NET MVC 网站在自定义表单身份验证期间可以使用它。
让我们从盐的创建过程开始。我们需要生成一个随机数,为此,我们可以使用 `RNGCryptoServiceProvider` 类。这个类确保生成的数字始终是随机和唯一的。所以,让我们实现一个简单的类,它将使用这个 `RNGCryptoServiceProvider` 来生成一个随机数。
public static class SaltGenerator
{
private static RNGCryptoServiceProvider m_cryptoServiceProvider = null;
private const int SALT_SIZE = 24;
static SaltGenerator()
{
m_cryptoServiceProvider = new RNGCryptoServiceProvider();
}
public static string GetSaltString()
{
// Lets create a byte array to store the salt bytes
byte[] saltBytes = new byte[SALT_SIZE];
// lets generate the salt in the byte array
m_cryptoServiceProvider.GetNonZeroBytes(saltBytes);
// Let us get some string representation for this salt
string saltString = Utility.GetString(saltBytes);
// Now we have our salt string ready lets return it to the caller
return saltString;
}
}
现在我们有了一个可以为我们生成随机盐值的类。现在,让我们创建一个负责对消息进行哈希处理的类。我们将使用 `SHA256CryptoServiceProvider` 类来生成哈希值。有很多算法可供选择。让我们看看如何为字符串消息生成哈希值。
public class HashComputer
{
public string GetPasswordHashAndSalt(string message)
{
// Let us use SHA256 algorithm to
// generate the hash from this salted password
SHA256 sha = new SHA256CryptoServiceProvider();
byte[] dataBytes = Utility.GetBytes(message);
byte[] resultBytes = sha.ComputeHash(dataBytes);
// return the hash string to the caller
return Utility.GetString(resultBytes);
}
}
现在我们有了两个类
SaltGenerator
:这个类的职责是每次都生成一个随机且唯一的盐。HashComputer
:这个类的职责是为给定的字符串消息生成 `SHA256` 哈希值。
现在,让我们看看以算法形式的加盐和哈希过程。
用户创建过程:
- 用户输入密码。
- 为用户生成一个随机盐值。
- 将盐值添加到密码中,并生成最终字符串。
- 计算最终字符串的哈希值。
- 将哈希值和盐值存储在用户的数据库中。
用户尝试登录:
- 用户输入其用户 ID。
- 使用用户 ID 从数据库检索用户的密码哈希值和盐值。
- 用户输入其密码。
- 将检索到的盐添加到此密码中,并生成最终字符串。
- 计算最终字符串的哈希值。
- 将计算出的哈希值与从数据库检索到的哈希值进行比较。
- 如果匹配,则密码正确,否则不正确。
因此,让我们在这些类之上创建一个外观类,它将完成大部分工作。它将获取用户的 ID 和密码,然后负责处理其余的。它将向用户返回哈希值和盐值。在密码检索的情况下,它将询问用户其密码和盐值,然后检查它们是否匹配。
public class PasswordManager
{
HashComputer m_hashComputer = new HashComputer();
public string GeneratePasswordHash(string plainTextPassword, out string salt)
{
salt = SaltGenerator.GetSaltString();
string finalString = plainTextPassword + salt;
return m_hashComputer.GetPasswordHashAndSalt(finalString);
}
public bool IsPasswordMatch(string password, string salt, string hash)
{
string finalString = password + salt;
return hash == m_hashComputer.GetPasswordHashAndSalt(finalString);
}
}
现在,我们有了一个简单的类库,它能够对用户密码进行加盐和哈希。使用这个库并将值保存在数据库中将是应用程序的责任。这个类库的类图如下所示:
注意:`Utility` 类包含 2 个简单的将 `string` 转换为 `byte[]` 及其反向转换的方法。
创建测试应用程序
现在,让我们继续创建一个简单的测试应用程序来测试这个库。让我们创建一个简单的控制台应用程序。让我们先创建一个 `Model` 类,它将保存用户登录数据。
class User
{
public string UserId { get; set; }
public string PasswordHash { get; set; }
public string Salt { get; set; }
}
注意:这是一个非常基础的用户登录详细信息模型。它仅用于解释文章中的概念。真实世界的应用程序将包含更详细的模型。
这个模型类代表了用户登录详细信息表中任何用户的用户详细信息。我们将在创建新用户以及用户尝试登录时使用此模型。
现在,让我们尝试模拟/mock 数据库功能。让我们创建一个简单的存储库类,它将所有数据保存在内存中,而不是推送到数据库。真正的存储库类应该在相应的操作中将数据推送到数据库。
class MockUserRepository
{
List<user> users = new List<user>();
// Function to add the user to im memory dummy DB
public void AddUser(User user)
{
users.Add(user);
}
// Function to retrieve the user based on user id
public User GetUser(string userid)
{
return users.Single(u => u.UserId == userid);
}
}
最后,让我们编写一些代码来模拟和测试用户创建和密码比较功能。
class Program
{
// Dummy repository class for DB operations
static MockUserRepository userRepo = new MockUserRepository();
// Let us use the Password manager class to generate the password ans salt
static PasswordManager pwdManager = new PasswordManager();
static void Main(string[] args)
{
// Let us first test the password hash creation i.e. User creation
string salt = SimulateUserCreation();
// Now let is simualte the password comparison
SimulateLogin(salt);
Console.ReadLine();
}
public static string SimulateUserCreation()
{
Console.WriteLine("Let us first test the password hash creation i.e. User creation");
Console.WriteLine("Please enter user id");
string userid = Console.ReadLine();
Console.WriteLine("Please enter password");
string password = Console.ReadLine();
string salt = null;
string passwordHash = pwdManager.GeneratePasswordHash(password, out salt);
// Let us save the values in the database
User user = new User
{
UserId = userid,
PasswordHash = passwordHash,
Salt = salt
};
// Lets Add the User to the database
userRepo.AddUser(user);
return salt;
}
public static void SimulateLogin(string salt)
{
Console.WriteLine("Now let is simulate the password comparison");
Console.WriteLine("Please enter user id");
string userid = Console.ReadLine();
Console.WriteLine("Please enter password");
string password = Console.ReadLine();
// Let us retrieve the values from the database
User user2 = userRepo.GetUser(userid);
bool result = pwdManager.IsPasswordMatch(password, user2.Salt, user2.PasswordHash);
if (result == true)
{
Console.WriteLine("Password Matched");
}
else
{
Console.WriteLine("Password not Matched");
}
}
}
现在让我们尝试创建一个用户,然后输入一个有效的密码
现在让我们尝试输入错误的密码一次
这样,我们就看到了密码加盐和哈希的基础知识,并创建了一个简单的可重用类库,可用于实现这一点。
关注点
在本文中,我们讨论了通过对用户密码使用加盐和哈希来保存用户密码的推荐方法。我们创建了一个简单的可重用类库,可用于生成盐值和哈希用户密码。我强烈建议在任何时候使用自定义表单身份验证时都使用这种技术。
本文是从初学者的角度撰写的。我希望它具有启发性。
历史
- 2013 年 6 月 19 日:初版。