【问题标题】:Hardcode C# byte property into a model将 C# 字节属性硬编码到模型中
【发布时间】:2020-02-07 14:35:32
【问题描述】:

我正在编写 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);
        }
}

我这里把PasswordHashPasswordSalt设为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; }
}

如果您发现整体测试逻辑很奇怪,请告诉我如何进行此测试并留下一些反馈。这是我第一次尝试编写单元测试。

【问题讨论】:

  • 要么获取您的应用程序用于散列的 Salt 值和使用的加密算法,以便您可以获取 PasswordSaltPasswordHash 的字节数组,或者不要调用 VerifyPasswordHash()。没有看到 VerifyPasswordHash 做了什么,很难提供比我评论更多的帮助。这篇文章展示了加盐和散列的工作原理 (stackoverflow.com/questions/2138429/…)
  • 那么,passwordSaltpasswordHash 如何在单元测试的外部 填充?我假设给定一个密码和一个(随机)盐,计算一个散列,并存储盐和散列。为什么不能在单元测试中这样做?
  • 您好,谢谢。我添加了 VerifyPasswordHash 方法。我正在考虑在用户注册时将创建Hash和Salt的方法添加到数据库中,但真的不想让这个问题看起来太复杂。
  • 恭喜。这是我在 LOOOONG 时间内看到的第一个问题,以实际展示对正确密码处理的理解。为此 +1。
  • 我们需要将问题中的代码复制/粘贴到我们的答案中是很常见的。如果您发布图像,我们必须重新输入。这通常足以让某人完全跳过问题并继续下一个问题。粘贴代码文本而不是制作图像也减少了工作量。最后,一小部分用户甚至看不到图像。

标签: c# arrays unit-testing asp.net-core xunit


【解决方案1】:

我会将用于创建哈希值的代码分解到它自己的方法中,您可以单独进行单元测试。

所以这个:

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();
}

【讨论】:

    猜你喜欢
    • 2015-06-21
    • 2010-10-27
    • 2015-06-13
    • 1970-01-01
    • 2012-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多