【问题标题】:How can I store my users' passwords safely?如何安全地存储我的用户密码?
【发布时间】:2010-12-07 14:03:39
【问题描述】:

这比普通的MD5 安全多少?我刚刚开始研究密码安全性。我对 PHP 很陌生。

$salt = 'csdnfgksdgojnmfnb';

$password = md5($salt.$_POST['password']);
$result = mysql_query("SELECT id FROM users
                       WHERE username = '".mysql_real_escape_string($_POST['username'])."'
                       AND password = '$password'");

if (mysql_num_rows($result) < 1) {
    /* Access denied */
    echo "The username or password you entered is incorrect.";
} 
else {
    $_SESSION['id'] = mysql_result($result, 0, 'id');
    #header("Location: ./");
    echo "Hello $_SESSION[id]!";
}

【问题讨论】:

  • 注意 php 5.4+ 内置了这个
  • 另见 Openwall 的 PHP password hashing framework (PHPass)。它的便携性和强化了对用户密码的一些常见攻击。
  • 强制性“使用PDOs而不是字符串插值”,对于今天偶然发现这个问题的人。

标签: php security passwords salt password-hash


【解决方案1】:

确保密码存储方案安全的最简单方法是使用标准库

由于安全性往往比大多数程序员单独解决的问题要复杂得多,而且存在更多不可见的搞砸可能性,因此使用标准库几乎总是最简单、最安全(如果不是唯一的话)的可用选项。


新的 PHP 密码 API (5.5.0+)

如果您使用的是 PHP 5.5.0 或更新版本,您可以使用新的简化密码哈希 API

使用 PHP 密码 API 的代码示例:

<?php
// $hash is what you would store in your database
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT, ['cost' => 12]);

// $hash would be the $hash (above) stored in your database for this user
$checked = password_verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

(如果您仍在使用旧版 5.3.7 或更高版本,您可以安装 ircmaxell/password_compat 以访问内置功能)


改进盐渍哈希:添加胡椒

如果您想要额外的安全性,安全人员现在(2017 年)建议将“pepper”添加到(自动)加盐密码哈希中。

有一个简单的类可以安全地实现这种模式,我建议: Netsilik/PepperedPasswords (github)。
它带有 MIT 许可证,因此您可以随心所欲地使用它,即使在专有项目中也是如此。

使用Netsilik/PepperedPasswords的代码示例:

<?php
use Netsilik/Lib/PepperedPasswords;

// Some long, random, binary string, encoded as hexadecimal; stored in your configuration (NOT in your Database, as that would defeat the entire purpose of the pepper).
$config['pepper'] = hex2bin('012345679ABCDEF012345679ABCDEF012345679ABCDEF012345679ABCDEF');

$hasher = new PepperedPasswords($config['pepper']);

// $hash is what you would store in your database
$hash = $hasher->hash($_POST['password']);

// $hash would be the $hash (above) stored in your database for this user
$checked = $hasher->verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}


旧标准库

请注意:您应该不再需要这个了!这只是出于历史目的。

看看:Portable PHP password hashing framework:phpass 并确保尽可能使用CRYPT_BLOWFISH 算法。

使用 phpass (v0.2) 的代码示例:

<?php
require('PasswordHash.php');

$pwdHasher = new PasswordHash(8, FALSE);

// $hash is what you would store in your database
$hash = $pwdHasher->HashPassword( $password );

// $hash would be the $hash (above) stored in your database for this user
$checked = $pwdHasher->CheckPassword($password, $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

PHPPass 已在一些非常知名的项目中实现:

  • phpBB3
  • WordPress 2.5+ 以及 bbPress
  • Drupal 7 版本,(可用于 Drupal 5 和 6 的模块)
  • others

好处是您不必担心细节,这些细节已经由有经验的人编写,并由互联网上的许多人审查。

有关密码存储方案的更多信息,请阅读Jeff的博文:You're Probably Storing Passwords Incorrectly

如果你选择“我会自己做,谢谢”的方法,无论你做什么,不要再使用MD5SHA1。它们是不错的散列算法,但出于安全目的被认为已损坏

目前,使用 crypt 和 CRYPT_BLOWFISH 是最佳做法。
PHP 中的 CRYPT_BLOWFISH 是 Bcrypt 哈希的实现。 Bcrypt 基于 Blowfish 分组密码,利用其昂贵的密钥设置来降低算法速度。

【讨论】:

    【解决方案2】:

    如果您使用参数化查询而不是连接 SQL 语句,您的用户会更加安全。 the salt 对于每个用户应该是唯一的,并且应该与密码哈希一起存储。

    【讨论】:

    • Nettuts+ 上有一篇关于 PHP 安全性的好文章,还提到了密码加盐。也许你应该看看:net.tutsplus.com/tutorials/php/…
    • Nettuts+ 是一篇非常糟糕的文章,不适合用作模型 - 它包括使用 MD5,即使使用盐也很容易被暴力破解。相反,只需使用 PHPass 库,它比您在教程网站上找到的任何代码都要好得多,即这个答案:stackoverflow.com/questions/1581610/…
    【解决方案3】:

    更好的方法是让每个用户都有一个独特的盐。

    使用盐的好处是,攻击者更难预先生成每个字典单词的 MD5 签名。但是如果攻击者知道你有一个固定的盐,他们就可以预先生成每个以你的固定盐为前缀的字典单词的 MD5 签名。

    更好的方法是每次用户更改密码时,您的系统都会生成一个随机盐并将该盐与用户记录一起存储。检查密码的成本有点高(因为您需要在生成 MD5 签名之前查找盐),但它使攻击者更难预生成 MD5。

    【讨论】:

    • 盐通常与密码哈希一起存储(例如,crypt() 函数的输出)。而且由于无论如何您都必须检索密码哈希,因此使用用户特定的盐不会使该过程变得更加昂贵。 (或者你的意思是生成一个新的随机盐很贵?我真的不这么认为。)否则+1。
    • 为了安全起见,您可能希望仅通过存储过程提供对表的访问,并防止哈希值被返回。相反,客户端传递它认为是散列的内容并获得成功或失败标志。这允许存储过程记录尝试、创建会话等。
    • @Inshallah - 如果所有用户都有相同的盐,那么你可以重用你对 user1 使用的字典攻击 user2。但是如果每个用户都有一个唯一的盐,你需要为每个你想要攻击的用户生成一个新的字典。
    • @R Samuel - 这正是我投票支持您的答案的原因,因为它推荐了避免此类攻击的最佳实践策略。我的评论是为了表达我对你所说的关于每用户盐的额外成本的困惑,我根本不明白。 (由于“盐通常与密码哈希一起存储”,每个用户对盐的任何额外存储和 CPU 需求都非常微小,甚至不需要提及......)
    • @Inshallah - 我在考虑这样一种情况,即您检查数据库的哈希密码是否正常(然后您有一个数据库检索来获取盐,而第二个数据库访问权限来检查哈希密码)。您在一次检索中下载盐/哈希密码然后在客户端上进行比较的情况是正确的。很抱歉造成混乱。
    【解决方案4】:

    随着 PHP 5.5(我所描述的甚至更早的版本也可用,见下文)即将到来,我想建议使用其新的内置解决方案:password_hash()password_verify()。它提供了几个选项来实现您需要的密码安全级别(例如,通过$options 数组指定“成本”参数)

    <?php
    var_dump(password_hash("my-secret-password", PASSWORD_DEFAULT));
    
    $options = array(
        'cost' => 7, // this is the number of rounds for bcrypt
        // 'salt' => 'TphfsM82o1uEKlfP9vf1f', // you could specify a salt but it is not recommended
    );
    var_dump(password_hash("my-secret-password", PASSWORD_BCRYPT, $options));
    ?>
    

    会回来

    string(60) "$2y$10$w2LxXdIcqJpD6idFTNn.eeZbKesdu5y41ksL22iI8C4/6EweI7OK."
    string(60) "$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d."
    

    如您所见,字符串包含盐以及选项中指定的成本。它还包含使用的算法。

    因此,在检查密码时(例如当用户登录时),当使用免费的password_verify() 函数时,它会从密码哈希本身中提取必要的加密参数。

    当不指定盐时,每次调用password_hash() 时生成的密码哈希都会不同,因为盐是随机生成的。因此,即使密码正确,将之前的哈希值与新生成的哈希值进行比较也会失败。

    验证工作如下:

    var_dump(password_verify("my-secret-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));
    var_dump(password_verify("wrong-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));
    
    var_dump(password_verify("my-secret-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));
    var_dump(password_verify("wrong-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));
    

    我希望提供这些内置函数将很快提供更好的密码安全性以防数据被盗,因为它减少了程序员必须投入到正确实现中的思考量。

    有一个小型库(一个 PHP 文件)可以在 PHP 5.3.7+ 中为您提供 PHP 5.5 的 password_hashhttps://github.com/ircmaxell/password_compat

    【讨论】:

    • 在大多数情况下,最好省略 salt 参数。该函数从操作系统的随机来源创建盐,您自己提供更好的盐的机会很小。
    • 那是我写的,不是吗? “如果没有指定盐,它是随机生成的,因此最好不要指定盐”
    • 大多数示例显示了如何添加这两个参数,即使不建议添加盐,所以我想知道为什么?老实说,我只阅读了代码后面的注释,而不是下一行。无论如何,如果示例展示了如何最好地使用该功能,那不是更好吗?
    • 我应该如何检查保存的密码和输入的密码是否相同?我使用password_hash()password_verify无论我使用什么密码(正确与否)我最终都会得到正确的密码
    • 对于password_verify,第一个参数是用户输入的内容,第二个是来自数据库的哈希值。如果密码正确,它应该只返回 true。我不确定你的问题是什么?
    【解决方案5】:

    这对我来说很好。 Atwood 先生写了关于 the strength of MD5 against rainbow tables 的文章,基本上像这样的长盐,你坐得很漂亮(虽然一些随机的标点符号/数字,它可以改善它)。

    您还可以查看 SHA-1,它最近似乎越来越流行。

    【讨论】:

    • @Matthew Scharley:我不同意昂贵的密码散列算法所带来的额外努力是错误的安全性。这是为了防止暴力破解容易猜到的密码。如果您要限制登录尝试,那么您就是在防止同样的事情(尽管更有效一些)。但是,如果攻击者可以访问数据库存储的哈希值,他将能够相当快地暴力破解这种(容易猜到的)密码(取决于容易猜到的程度)。 SHA-256 crypt 算法的默认值是 10000 轮,因此它的难度会增加 10000 倍。
    • 慢速散列实际上是通过对快速散列进行大量迭代,并在每次迭代之间对数据进行洗牌而产生的。目标是确保即使坏人获得了您的密码哈希副本,他也必须消耗大量 CPU 时间来根据您的哈希测试他的字典。
    • @caf:我相信 bcrypt 算法利用了 Eksblowfish 密钥调度的参数化昂贵性;不完全确定这是如何工作的,但密钥调度通常是在初始化密码上下文对象期间完成的非常昂贵的操作,在任何加密完成之前。
    • Inshallah:这是真的 - bcrypt 算法是一种不同的设计,其中底层加密原语是块密码而不是散列函数。我指的是基于散列函数的方案,例如 PHK 的 MD5 crypt()。
    【解决方案6】:

    我要补充:

    • 不要按长度限制用户密码

    为了与旧系统兼容,通常会设置密码的最大长度限制。这是一个糟糕的安全策略:如果您设置了限制,请仅将其设置为密码的最小长度。

    • 不要通过电子邮件发送用户密码

    为了找回忘记的密码,您应该发送用户可以更改密码的地址。

    • 更新用户密码的哈希值

    密码哈希可能已过期(算法参数可能会更新)。使用password_needs_rehash()函数可以查看。

    【讨论】:

      【解决方案7】:

      【讨论】:

      猜你喜欢
      • 2010-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-13
      • 2016-04-01
      • 1970-01-01
      • 2017-06-01
      • 1970-01-01
      相关资源
      最近更新 更多