使用PBKDF2算法来创建哈希的方法。PBKDF2全称Password-Based Key Derivation Function,它的基本原理是通过一个伪随机函数(例如HMAC函数),把明文和一个盐值作为输入参数,然后重复进行运算,最终产生秘钥。如果重复的次数足够大,破解的成本将非常大。
PBKDF2定义如下
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
- PRF 是一个伪随机函数,可以简单的理解为 Hash 函数。
- Password 表示口令 。
- Salt 表示盐值,一个随机数。
- c 表示迭代次数。
- dkLen 表示最后输出的密钥长度。 我们先引入NuGet包
Install-Package Microsoft.AspNetCore.Cryptography.KeyDerivation -Version 3.1.6
我们创建一个加密密码的方法,传入需要加密密码,和盐。参数的含义已经写在注释里了。
private static string HashPassword(string value, string salt)
{
var valueBytes = KeyDerivation.Pbkdf2(
password: value,//密码
salt: Encoding.UTF8.GetBytes(salt),//盐
prf: KeyDerivationPrf.HMACSHA512,//伪随机函数,这里是SHA-512
iterationCount: 10000,//迭代次数
numBytesRequested: 256 / 8);//最后输出的秘钥长度
return Convert.ToBase64String(valueBytes);
}
现在密码有了,还缺点盐,我们再写一个随机盐生成的方法。
private static string GenerateSalt()
{
byte[] randomBytes = new byte[128 / 8];
using (var generator = RandomNumberGenerator.Create())
{
generator.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
好了,现在我们有了密码,有了盐,也有了加密好的hash值,那么用户登录的时候,如何校验密码是否正确呢,我们再写一个hash校验的方法。这时我们需要输入用户输入的密码,盐,以及生成好的hash值,就可以判断用户输入的密码是否正确了。
private static bool Validate(string password, string salt, string hash)
=> HashPassword(password, salt) == hash;
至此,内部的密码加密和校验部分已经完成了,但还有一个问题,就是创建用户密码的时候,如何存储的用户密码的hash值和盐呢,这里我采取使用“点”进行拼接存储,即使用salt.hash的形式,相应的,取值校验,再以“点”进行拆分。具体代码如下
public static string HashPassword(string password)
{
var salt = GenerateSalt();
var hash = HashPassword(password, salt);
var result = $"{salt}.{hash}";
Console.WriteLine("hash result:{0}", result);
return result;
}
相应的,我们再提供一个对外的密码校验函数
public static bool VerifyHashedPassword(string password, string storePassword)
{
if (string.IsNullOrEmpty(password))
{
throw new ArgumentNullException(nameof(password));
}
if (string.IsNullOrEmpty(storePassword))
{
throw new ArgumentNullException(nameof(storePassword));
}
var parts = storePassword.Split('.');
var salt = parts[0];
var hash = parts[1];
return Validate(password, salt, hash); ;
}
至此,所有代码编写完成,我们下面来试试效果。
我们输入一个简单的密码,1234,循环了5次,模拟5个用户都使用1234这个密码,我们看一下效果。
var password = "1234";
for (int i = 0; i < 5; i++)
{
var result = PasswordHasher.HashPassword(password);
var right = PasswordHasher.VerifyHashedPassword(password, result);
Console.WriteLine("password is right:{0}", right);
}
可以看出这五条数据虽然拥有相同的密码,但是拥有不同的盐和hash值。
- 完整的代码如下
public class PasswordHasher
{
private static string HashPassword(string value, string salt)
{
var valueBytes = KeyDerivation.Pbkdf2(
password: value,
salt: Encoding.UTF8.GetBytes(salt),
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: 10000,
numBytesRequested: 256 / 8);
return Convert.ToBase64String(valueBytes);
}
public static string HashPassword(string password)
{
var salt = GenerateSalt();
var hash = HashPassword(password, salt);
var result = $"{salt}.{hash}";
Console.WriteLine("hash result:{0}", result);
return result;
}
private static bool Validate(string password, string salt, string hash)
=> HashPassword(password, salt) == hash;
public static bool VerifyHashedPassword(string password, string storePassword)
{
if (string.IsNullOrEmpty(password))
{
throw new ArgumentNullException(nameof(password));
}
if (string.IsNullOrEmpty(storePassword))
{
throw new ArgumentNullException(nameof(storePassword));
}
var parts = storePassword.Split('.');
var salt = parts[0];
var hash = parts[1];
return Validate(password, salt, hash); ;
}
private static string GenerateSalt()
{
byte[] randomBytes = new byte[128 / 8];
using (var generator = RandomNumberGenerator.Create())
{
generator.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
}