应该为每个用户随机生成一个新的盐,并且每次他们至少更改密码。例如,不要仅仅依赖站点范围的盐,因为这首先破坏了使用盐的意义。
为每个用户使用唯一的盐是这样如果两个用户有相同的密码,他们将不会得到相同的结果哈希。这也意味着需要单独针对每个用户进行蛮力攻击,而不是能够为站点预先计算 rainbow table。
然后,您将盐和密码的散列结果与每个用户的 salt 一起存储在数据库 hash(salt + password) 中。您可以将它们存储在单独的列中,或全部存储在一列中(由散列中未使用的某些字符分隔,例如 ;)。只要你能检索到你会没事的。
但是,如果您的数据库受到破坏,无论是由于某人获得本地访问权限还是通过 SQL 注入攻击,那么盐和最终哈希都将可用,这意味着对用户密码的暴力攻击将是微不足道的。为了解决这个问题,正如The Rook 所建议的那样,您还可以使用存储在本地文件中的站点级密钥作为散列方法的另一个输入,这样攻击者也需要知道这一点才能发起有效的攻击。这意味着您的数据库必须受到损害,并且攻击者需要访问本地文件。所以使用hash(hash(salt + secret) + password)等
虽然在大多数算法中,您的目标是使事情尽可能快,但对于密码散列,您希望减慢它的速度,这称为Key Strengthening(或有时是密钥拉伸)。如果您的哈希函数需要 0.00001 秒才能返回,那么有人可以尝试每秒暴力破解 100,000 个密码,直到找到匹配项。如果您的哈希函数需要 1 秒钟的时间来吐出结果,那么就登录您的应用程序的人而言,这没什么大不了的,但是对于破解密码来说,这是一个更大的问题,因为现在每次尝试都需要 1 秒钟才能得到结果,这意味着测试每个暴力破解密码所需的时间是使用原始哈希函数的 100,000 倍。
要让你的哈希函数变慢,你只需要运行它多次。例如,您可以执行new_hash = salt + password + previous_hash 100,000 次。如果太快,您可能需要将迭代次数调整为更高的值。如果您希望以后能够更改该值,请确保将迭代次数与用户记录一起存储,以免影响之前存储的任何密码。
您的用户记录现在应该有一个格式类似于“$<algorithm>$<iterations>$<salt>$<hash>”的字段(如果需要,也可以作为单独的字段)。
当用户输入他们的密码时,您可以从数据库中检索盐和迭代次数,并从本地文件中检索站点范围的秘密,并验证当您使用盐和密码运行相同数量的迭代时,结果哈希匹配您存储的内容。
如果用户更改了他们的密码,那么你应该生成一个新的盐。
您使用的散列方法无关紧要(但散列算法可以*)。上面我建议hash(hash(salt + secret) + password),但同样可以是hash(hash(salt) + hash(secret) + hash(password))。您使用的方法不会改变密码存储的有效性,其中一种方法实际上并不比另一种更安全。依赖于如何将密码和盐一起散列以提供安全性的设计称为security through obscurity,应该避免。
*您不应使用 MD5 或 SHA-1,因为它们被认为是不安全的。请改用 SHA-2 系列(SHA256、SHA512 等)。 (Ref)