将 C# 字节 属性 硬编码到模型中
Hardcode C# byte property into a model
我正在编写 xunit 来测试 Authenticate
方法。非常简单:
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
var user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
VerifyPasswordHash
方法:
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
但要对此进行测试,我需要在我的数据库中植入一些 User
实体。
这就是我想要做的:
public void TestAuthenticate()
{
//Arrange
var options = new DbContextOptionsBuilder<DataContext>() //instead of mocking we use inMemoryDatabase.
.UseInMemoryDatabase(databaseName: "TestAuthenticate")
.Options;
var config = new MapperConfiguration(cfg =>
cfg.AddProfile<AutoMapperProfile>());
var mapper = config.CreateMapper();
var fakeUser = new User()
{
Username = "anon1", FirstName = "fakename", LastName = "fakelastname", Role = "admin", PasswordHash = null, PasswordSalt = null
};
using (var context = new DataContext(options))
{
context.Users.Add(fakeUser);
context.SaveChanges();
}
// Act
using (var context = new DataContext(options))
{
var service = new UserService(context, mapper);
var result = service.Authenticate(fakeUser.Username, "somepassword");
// Assert
Assert.IsType<User>(result);
}
}
我在这里让PasswordHash
和PasswordSalt
为null,但它们应该是byte[],这是它们在数据库中的存储方式:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string Role { get; set; }
}
请让我知道如何进行此测试,如果您发现整体测试逻辑很奇怪,请留下一些反馈。这是我第一次尝试编写单元测试。
我会将创建哈希值的代码提取到它自己的方法中,您可以单独进行单元测试。
所以这个:
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
变成这样:
private static byte[] ComputeHash(string data, byte[] salt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512(salt))
{
return hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
}
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
var computedHash = ComputeHash(password, storedSalt);
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
return true;
}
这样做有几个目的:它允许您与代码共享此方法以在创建、更改和重置时生成密码哈希值,使 确定 代码初始哈希密码使用与代码相同的过程来验证哈希值;它可以让您隔离哈希生成以进行单独的单元测试;如果 sha512 不再可行,调整散列算法会更安全、更容易。这还有其他原因。
虽然我在这里,但我可能还会向用户添加一个 authType
字段,如果 sha512 不再可行,这将使调整此算法更容易和更安全,甚至有两个不同的进程同时激活。例如,如果您需要与外部 OAuth 或 SAML 服务集成,您可能需要一个单独的过程。
有了 ComputeHash()
函数后,您应该做一些类似的事情来创建一个 GenerateRandomSalt()
函数,以便在创建新用户时调用。有了这些 两者,为完整身份验证过程的单元测试创建参考数据就容易多了:
var fakeUser = new User()
{
Username = "anon1", FirstName = "fakename", LastName = "fakelastname",
Role = "admin", PasswordHash = null, PasswordSalt = GenerateRandomSalt()
};
fakeUser.PasswordHash = ComputeHash("somepassword", fakeUser.PasswordSalt);
using (var context = new DataContext(options))
{
context.Users.Add(fakeUser);
context.SaveChanges();
}
我正在编写 xunit 来测试 Authenticate
方法。非常简单:
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
var user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
VerifyPasswordHash
方法:
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
但要对此进行测试,我需要在我的数据库中植入一些 User
实体。
这就是我想要做的:
public void TestAuthenticate()
{
//Arrange
var options = new DbContextOptionsBuilder<DataContext>() //instead of mocking we use inMemoryDatabase.
.UseInMemoryDatabase(databaseName: "TestAuthenticate")
.Options;
var config = new MapperConfiguration(cfg =>
cfg.AddProfile<AutoMapperProfile>());
var mapper = config.CreateMapper();
var fakeUser = new User()
{
Username = "anon1", FirstName = "fakename", LastName = "fakelastname", Role = "admin", PasswordHash = null, PasswordSalt = null
};
using (var context = new DataContext(options))
{
context.Users.Add(fakeUser);
context.SaveChanges();
}
// Act
using (var context = new DataContext(options))
{
var service = new UserService(context, mapper);
var result = service.Authenticate(fakeUser.Username, "somepassword");
// Assert
Assert.IsType<User>(result);
}
}
我在这里让PasswordHash
和PasswordSalt
为null,但它们应该是byte[],这是它们在数据库中的存储方式:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string Role { get; set; }
}
请让我知道如何进行此测试,如果您发现整体测试逻辑很奇怪,请留下一些反馈。这是我第一次尝试编写单元测试。
我会将创建哈希值的代码提取到它自己的方法中,您可以单独进行单元测试。
所以这个:
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
变成这样:
private static byte[] ComputeHash(string data, byte[] salt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512(salt))
{
return hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
}
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
var computedHash = ComputeHash(password, storedSalt);
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
return true;
}
这样做有几个目的:它允许您与代码共享此方法以在创建、更改和重置时生成密码哈希值,使 确定 代码初始哈希密码使用与代码相同的过程来验证哈希值;它可以让您隔离哈希生成以进行单独的单元测试;如果 sha512 不再可行,调整散列算法会更安全、更容易。这还有其他原因。
虽然我在这里,但我可能还会向用户添加一个 authType
字段,如果 sha512 不再可行,这将使调整此算法更容易和更安全,甚至有两个不同的进程同时激活。例如,如果您需要与外部 OAuth 或 SAML 服务集成,您可能需要一个单独的过程。
有了 ComputeHash()
函数后,您应该做一些类似的事情来创建一个 GenerateRandomSalt()
函数,以便在创建新用户时调用。有了这些 两者,为完整身份验证过程的单元测试创建参考数据就容易多了:
var fakeUser = new User()
{
Username = "anon1", FirstName = "fakename", LastName = "fakelastname",
Role = "admin", PasswordHash = null, PasswordSalt = GenerateRandomSalt()
};
fakeUser.PasswordHash = ComputeHash("somepassword", fakeUser.PasswordSalt);
using (var context = new DataContext(options))
{
context.Users.Add(fakeUser);
context.SaveChanges();
}