为什么您需要存储密码,甚至是加密版本?您的站点是否在后端访问执行 HTTP Basic auth 或其他内容的第 3 方 API?
不幸的是,您的问题没有明确的答案。“合适”对不同的人意味着不同的东西。对于安全问题,我不确定是否有可能“足够合适”或“足够安全”。也就是说,这就是我处理登录名和密码安全性的方式。在我的数据库users
表中,我有 3 个与登录相关的列:
- 用户名
- 盐
- 密码哈希
该username
列是纯文本格式。该salt
列是由随机选择的字母数字字符组成的 64 个字符的字符串。该passwordHash
列是用户的密码与其salt
值连接,然后不可逆地散列。我使用 sha256 作为我的哈希值。盐是 64 个字符,因为这是 sha256 产生的。最好有一个至少与哈希值一样长的盐值,以便在哈希字符串中产生足够的可变性。
当用户提交登录表单时,我对用户名进行数据库查询。如果未找到用户名,我会向用户显示“无效的用户名和/或密码”错误。如果找到用户名,我将盐与密码连接起来,对其进行哈希处理,并查看它是否等于该passwordHash
值。如果不是,则向用户显示完全相同的错误。
无论用户名是否错误,密码是否错误,或两者兼而有之,都最好显示完全相同的错误消息。你给黑客的线索越少越好。此外,每当用户更改密码时,我也会给他们一个新密码salt
。在那个时候这样做真的很容易,而且它使盐值保持新鲜一点。
这种每个用户使用不同盐的系统称为动态加盐。如果黑客试图使用彩虹表对用户密码进行逆向工程,这会使黑客的工作变得非常复杂。更不用说以不可逆的散列形式存储密码对于防止任何人确定用户的密码有很长的路要走,即使他们有权访问数据库和 PHP 代码。
这也意味着如果您的用户忘记了密码,则无法找回密码。相反,您编写系统只是将其重置为一个新的随机确定的值,该值会发送给他们,并强烈鼓励他们在再次登录后立即更改密码。您甚至可以编写系统以在下次成功登录时强制执行此操作。
我要求密码至少为 8 个字符。理想情况下,它还应该包括数字和特殊字符,但我还没有决定我应该需要这个。也许我应该!
为了防止暴力攻击,我会跟踪过去 10 分钟内所有失败的登录。我根据每个 IP 地址跟踪它们。在 3 次登录尝试失败后,系统使用该sleep()
功能延迟响应进一步的登录尝试。我使用这样的代码块:
$delay = ($failedAttempts - 3);
if ($delay > 0) {
sleep($delay);
}
恕我直言,这比在多次失败后将用户锁定在他们的帐户之外要好得多。它减少了您将获得的客户支持查询的数量,并且对于根本不记得自己的密码的合法用户来说更加优雅。蛮力攻击需要每秒进行多次尝试才能获得任何形式的效率,因此n = x
基于延迟可以防止它们走得太远。
使用 PHP 会话跟踪登录会话。session_start()
加载您网站上的每个页面时调用。(如果您有一个公用文件,这真的很容易header.php
。)这使得$_SESSION变量可用。当用户成功登录时,您可以使用它来存储站点所需的任何信息,以便知道用户已登录。我通常使用他们的用户 ID、用户名,也许还有一些特定于站点的其他详细信息。但我在这里没有包含密码或它的哈希值。如果黑客以某种方式进入了存储在您服务器上的用户会话数据,他们仍然没有机会通过这种方式找到用户的密码。
发生以下两种情况之一时会发生注销:1) 用户的会话 cookie 被删除,例如通过清除浏览器缓存或有时仅通过关闭浏览器窗口,或 2) 您的服务器删除他们的会话数据。session_destroy()
当用户按下您网站上的“注销”按钮时,您可以通过调用强制后者发生。否则,您可以使会话在一段时间后自动过期。这可能涉及调整你的session.gc_*
参数php.ini
。
如果您在初始登录阶段后绝对必须知道用户密码,您可以将其存储在$_SESSION
. 当且仅当您的网站需要 SSL 连接并且您已经做到这一点时才这样做,这样该网站将无法在没有 SSL 连接的情况下运行。这样,密码就被加密了,因此可以防止数据包嗅探。但要知道,如果黑客获得了访问您服务器会话数据的权限,就会存在安全风险。