使用哈希密码验证用户的流程是什么?

4
我正在阅读这篇关于哈希密码的文章,看到了以下部分:
验证密码的步骤:
1. 从数据库中检索用户的盐值和哈希值。 2. 将盐值添加到输入的密码前,然后使用相同的哈希函数进行哈希处理。 3. 将输入的密码的哈希值与数据库中的哈希值进行比较。如果它们匹配,则密码正确。否则,密码不正确。
但我对此流程有些困惑,例如假设我拥有一个用户表(包含id、name、password和email),并且为了登录某个应用程序,我需要输入我的电子邮件地址和密码。
按照上述步骤,首先需要从数据库中获取该用户的盐值+哈希密码。
问题:
假设我正在使用简单的存储过程,唯一的方式就是这样吗?
    CREATE PROCEDURE [dbo].[sp_validate_user]

@us_email VARCHAR (MAX)
AS
BEGIN
 -- SET NOCOUNT ON added to prevent extra result sets from
 -- interfering with SELECT statements.
 SET NOCOUNT ON;

    -- Insert statements for procedure here

SELECT  us_id,
        us_name,
        us_pass,
        us_email


 FROM  Users
 WHERE us_email = @us_email

END

然后按照第二步和第三步进行操作:
    public static bool ValidatePassword(string inputPassword, string storedPassword)
    {
        // Extract the parameters from the hash
        char[] delimiter = { ':' };
        string[] split = storedPassword.Split(delimiter);
        int iterations = Int32.Parse(split[ITERATION_INDEX]);
        byte[] salt = Convert.FromBase64String(split[SALT_INDEX]);
        byte[] hash = Convert.FromBase64String(split[PBKDF2_INDEX]);

        byte[] testHash = PBKDF2(inputPassword, salt, iterations, hash.Length);
        return SlowEquals(hash, testHash);
    }

我的担忧源于这样一个事实,如果我使用从表格中获取的数据创建对象,那么这不会使得其中的信息变得容易受到攻击吗?

此外,这是否意味着使用此验证的唯一方法是仅基于用户名/电子邮件获取所有用户信息,只是为了在运行时检查输入密码和哈希密码是否匹配,并让该用户访问信息?

如果听起来有点困惑,我很抱歉,但若能得到任何见解,那将非常好。


不确定为什么你要分割存储的密码。它应该只是一个哈希值。盐在哈希函数之前添加到密码中,而不是之后。 - codenheim
3个回答

7
看起来你可能反过来思考了。在传递到哈希函数之前,将盐添加到明文密码中。将最终结果存储在数据库中。
通常,盐是用户名。对于每个用户都是唯一的东西,以防止字典攻击。(字典攻击依赖规模经济,通过破解一个密码然后查找相同加密文本的其他实例来工作。它在非常大的用户数据库上特别有效,例如那些拥有数百万用户的知名网站,但是希望这些网站现在使用适当的加盐和密钥派生)。
因此,对于用户名u,密码

p

,假设SHA2是哈希函数。连接u+

p

以获取带盐值,然后进行哈希运算。
hashtext = SHA2(u + p)     // in this case, + is concatenate

hashtext是您存储在数据库中的内容。

对于登录,用户输入他的用户名u2和密码p2

tryhash = SHA2(u2 + p2)

查询数据库,寻找与u2匹配的用户记录,其密码哈希值为tryhash

假设您有一个MVC操作接收loginViewModel,该模型填充了页面上输入的明文电子邮件或用户名以及明文密码:

var loginUser = new User(loginViewModel);
CalcHash(loginUser);

var realUser = users.Find(loginUser.username);
if(realUser.HashPassword == loginUser.HashPassword)
    // success

虽然也可以将哈希密码作为第二个参数添加到数据访问方法中,例如users.Find(username, hashPass),但通常不会这样做,因为即使密码失败,您仍需要访问用户记录以增加密码失败计数并锁定帐户。


即使访问记录失败,这是否构成了太大的风险?我在谈论一个非常小的、家庭/宠物项目应用程序。无论如何,最好做正确的事情。 - Code Grasshopper
例如,看一下MVC ASP.NET项目中的内置登录代码,或者一个示例的Membership或Identity实现。它们都会访问用户记录以更新特定的统计信息(如passwordFailures++等)。 - codenheim
盐应该是随机的,具有一定的长度,并且不应该从其他信息(如用户名)中派生。盐不是您必须与哈希一起存储的唯一信息(成本因素和算法),因此您无论如何都必须查询数据库。 - martinstoeckli
盐的目的是为了防止攻击者建立单个彩虹表以一次性获取所有密码。例如,如果您使用用户名作为盐,可能会出现以下问题:1)太短(用户名“bill”只会将彩虹表扩大4个字符),2)众所周知(可以为用户“admin”构建彩虹表),3)可预测(可以预先计算某些名称/电子邮件的彩虹表以在多个站点上使用)。还有其他可能的攻击方式,但实际上我们应该反过来问:为什么不尽力而为并使用随机盐呢? - martinstoeckli
2
如果我理解你的意思正确的话,你是指将用户名进行哈希处理,并将其用作盐吗?这至少可以缓解短盐的问题1,但其他问题并没有得到解决,因为它仍然是可预测的。在实践中,这可能影响不大,但我自己也不知道所有可能的攻击方式。最好还是使用随机盐,由于其他参数(例如成本因素),使用派生盐没有任何优势(你需要成本因素和算法来验证密码)。 - martinstoeckli
显示剩余3条评论

1
本文涉及ASP.NET(C#)密码哈希代码,但您似乎想使用数据库?
您需要关注三件事:用户的唯一键(用户名),所选的哈希算法以及向密码尝试添加盐(防止彩虹表攻击)。
要验证密码,您应创建一个SQL存储过程,该存储过程接受用户名和密码尝试作为参数。这些数据是明文,并已输入到Web表单中,通过Web服务器传递,并将通过存储过程传递到数据库服务器。
存储过程将执行以下操作:
1. 根据匹配用户名参数与用户名字段查找用户的数据行并选择存储的盐字段
2. 将(1)中的盐附加到密码参数上并对结果进行哈希
3. 根据匹配用户名参数与用户名字段和(2)中的哈希结果与哈希密码字段查找用户的数据行。
4. 如果没有找到行,则密码哈希不匹配且错误,因此返回适当的错误代码
5. 如果找到了行,则返回有用的用户数据,即名字,地址
如果存储过程处理了这一切,那么Web服务器就不需要知道盐或哈希算法是什么。在任何时候,哈希结果或盐都不会传输到数据库服务器之外。

为了辩护,我并没有说它严格相关,只是它引发了上述的思考。感谢解释! - Code Grasshopper

0

我认为您正确理解了,这是通常的工作流程:

  1. 通过用户名获取密码哈希值 SELECT password_hash FROM user WHERE email=?
  2. 从密码哈希值中提取盐,或从单独的字段中获取盐。
  3. 使用提取的盐计算输入密码的哈希值并比较哈希值。

验证密码无法在单个查询中完成,因为您首先必须提取盐。适当的哈希函数例如 PBKDF2BCryptSCrypt 通常不受数据库系统支持,因此您必须在代码中执行验证。除了盐之外,您还必须存储其他参数,如成本因素和算法(以保证未来兼容性),因此将所有这些参数存储在同一个数据库字段中是一个好主意。

盐应该是至少20个字符的随机字符串,因此使用用户名作为盐或从其他信息派生盐都是不安全的


在第二点中,您打算如何从密码哈希中提取盐? - Starjumper Tech SL
@System24Tech - 大多数密码哈希函数都可以自行完成,它们通常有一个用于哈希的函数password_hash()和另一个用于验证的函数password_verify(),后者将从哈希字符串中提取盐。这个答案解释了一种常见的格式。 - martinstoeckli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接