当前认证的一般最佳实践在NIST SP 800-63-3 数字身份指南标准中,特别是在SP-63B 认证和生命周期管理中。
这些 NIST 标准对开发人员来说很容易阅读,除了告诉你要做什么之外,它还讨论了你为什么想做某些事情。(如果您需要比 NIST 标准提供的更多详细信息,我们很乐意提供帮助。)
也就是说,让我们看看您当前的身份验证系统并解决您的问题:
注册页面提示用户输入用户名和密码
注册页面?
这可能打错了,您的意思是登录页面,但是如果您的网络上的任何人都可以访问注册页面,那么您需要明确定义一个单独的授权系统。例如,任何人都可以在任何在线商店注册一个帐户,但该帐户不允许您访问其网站中允许管理员更改其产品价格的部分。
服务器使用 SHA256 对密码进行哈希处理以创建等长的哈希值
此步骤不会增加任何安全性,也不会以任何有意义的方式提高系统性能,尤其是因为您使用的是 bcrypt,它无论如何都会丢弃密码的前 72 个字符之后的任何部分。
这不太可能,但是这一步可以减少熵。熵的减少在宏伟的计划中是微不足道的,并且要求用户已经在使用长且随机生成的密码,但由于这是一个不会提高安全性的额外步骤,我建议将其省略。
使用合理的工作因子生成随机盐。
我承认一开始我很困惑看到这一步。如果这是关于汽车维修的,那对我来说就像“添加了具有合理辛烷值的合成油”一样有意义。然而,在评论中透露,正在使用的特定 bcrypt 库是PyPI 托管的 bcrypt 库。
我找不到源代码(并且对使用空格来描述范围的语言过敏),但根据文档,似乎该库用于生成 bcrypt 参数的函数调用名为bcrypt.gensalt(workfactor)
,并且此方法本身需要工作factor 作为它的参数...扩展汽车维修的比喻,就好像有一个名为vehicle.refuel(viscosity)
, or的函数vehicle.changeoil(octane)
。
在身份验证和密码学术语中,盐是添加到明文中的随机值,使预先计算密码散列函数或密钥派生函数的输出变得不可行。盐本身并不是秘密。它唯一的优点是事先不知道。
盐的价值是以它的长度来衡量的,也称为它的熵。(密码学中的熵是一个微妙的话题,它不仅仅取决于长度,但对于任何创建盐的合理库,盐越长,它的熵就越大。)
另一方面,工作因素是特定于密钥拉伸算法的行话,例如这个 bcrypt KDF。工作因子定义了 CPU(或 GPU、FPGA 或 ASIC,当你是攻击者时)需要做多少工作,其主要特点是计算输出需要更长的时间,使用无法绕过的步骤,猜测或忽略。这似乎违反直觉;您希望您的应用程序尽可能快地运行,对吗?
好吧,一般风险是您的密码数据库将被泄露。顶级零售商一直在泄露密码,而您没有像顶级零售商那样花费数百万美元来保护应用程序,因此假设您的密码数据库最终也可能被泄露。
这里的权衡是,登录时,您的用户将不得不多等一秒钟。当攻击者破解您的密码时,他们每秒只能猜测一个密码,每个 CPU 投入到任务中。(有动机的攻击者可以采取一些捷径,但如果您只使用单轮 SHA256 而不是密钥拉伸算法,那么即使每秒 10 次猜测也比每秒数十亿次猜测要好得多。)
因此,盐分和功因数都是彼此无关的重要参数。很抱歉,您第一次接触这个概念是通过一个名字很差的函数。
哈希密码再次被哈希,这次是盐。(盐生成和哈希使用python的bcrypt)
轻微的挑剔,但这是因为我很彻底:bcrypt 的结果不是哈希,尽管很多人称它为哈希,如果你继续称它为 a,这里的每个人都会知道你在说什么哈希。结果是延长密码或派生密钥。
用户名、工作因子、盐和哈希存储在密码数据库中。
如果结果看起来像这样,那么您已经拥有了工作因子、盐和派生密钥:
$2a$08$0SN/h83Gt1jZMR6924.Kd.HaK3MyTDt/W8FCjUOtbY3Pmres5rsma
2a
是算法(支持 unicode 的 bcrypt)。
是08
工作因素。
接下来的 22 个字符 ,0SN/h83Gt1jZMR6924.Kd.
是盐。
其余的是派生密钥。
如果你得到的东西与你的 bcrypt 库不同,找一个不同的库。
当用户尝试登录时,输入的密码以相同的方式进行哈希处理,并与数据库中的记录进行比较。(用于比较的盐是使用输入的用户名作为密钥从密码数据库中获取的。)
伟大的。但是,您正在使用的库已经为您完成了繁重的工作,因此请充分利用您的库。该bcrypt.checkpw
函数将为您提取工作因子、盐和派生密钥。然后它将运行其 KDF 并比较结果。
使用这个库函数意味着,如果您决定将来更改默认工作因数,您不必有单独的代码来处理旧派生密钥,因为bcrypt.checkpw
可以自己找出所需的工作因数.
如果哈希值匹配,则会向用户提供一个 cookie,授予他们访问受限网页的权限。
从表面上看,这听起来很棒……但请确保 cookie 本身不包含授权。它应该包含一个随机生成的字符串(会话 ID),并且在访问站点时,服务器应该查看该会话 ID 并检查其自己的资源,以查看该会话 ID 是否被正确授权。过一会儿也清理会话 ID。
如果您的 cookie 包含“is_authorized=true”而不是服务器双重检查自己资源的会话 ID,那么人们可以制作自己的授权 cookie,并且永远不需要进行身份验证。
关注点:
在同一个数据库中保存哈希、工作因子和盐似乎是一种不好的做法。
盐和工作因素在任何方面都不敏感,但对于验证密码是必需的。充其量,任何隐藏盐的尝试都会导致隐匿安全,这根本不是任何类型的安全。您最好将工作因子和盐存储在派生密钥旁边,以免自己头疼,因为将它们分开不会阻止攻击者获取它们。
这种散列方法(合理地)是否确保没有人可以确定用户密码,包括我自己?
至于您作为 Web 开发人员的责任?是的。
并感谢您询问它是否合理地确保没有人可以确定密码。它们仍然可以在离线攻击中被猜到,但使用 bcrypt 等密钥拉伸算法将极大地阻碍攻击者,即使在对泄露数据库进行离线攻击的最坏情况下也是如此。
您唯一可以做得更好的是添加 2fa 并要求您的用户使用具有 24+ 个字符长的密码管理器,真正随机密码对于每个站点都是唯一的。
请注意,密码重置不应仅取决于第二个因素。根据 NIST 指南,它应该首先取决于设置帐户时使用的相同识别因素(并在您的应用程序知道的尽可能多的用户通信渠道上发出尽可能多的噪音)并至少使用一个额外的因素,如果有的话。
对于大多数 Web 应用程序,这意味着向帐户注册期间使用的帐户发送电子邮件(不是发送到手机的短信或安全问题本身,尽管这些可能是发送电子邮件后的其他因素)。对于内部业务应用程序,这意味着用户必须致电 sysops 团队/帮助台(或者,更实际地,他们需要在开发人员的办公桌前停下来)。
目前一切都使用http。如果所有访问都在本地网络内,是否需要 https?
您必须在某种程度上信任您的本地网络。但是,添加 HTTPS 永远不会有坏处。如果您完全担心,请添加 HTTPS。(您问这个问题的事实意味着,是的,您很担心。所以,是的,您应该添加 HTTPS。)
出于非安全原因,添加 HTTPS 也是一个好主意,因为 Web 服务器(和浏览器)将仅通过 HTTPS 连接使用 HTTP/2(如果可用)。这可以显着加快您的网站速度。它不适用于您的情况,因为您使用的是基于 Python 脚本的 Web 服务器,它不太可能具有 HTTP/2 功能,但出于“只是”安全性之外的原因进入是一个好习惯。(在我看来,安全性是绰绰有余的理由,但有些经理需要额外的说服力。)
使用 cookie 是验证用户是否成功登录的好方法吗?
如果该 cookie 包含会话 ID 而不是身份验证信息,则该会话 ID 是随机的,并且服务器会验证该会话 ID,那么是的。这是绝大多数具有任何类型用户标识的网站的标准做法。