如何安全地存储用户密码?


169

这比普通的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+已内置此功能
Benjamin Gruenbaum

另请参见Openwall的PHP密码哈希框架(PHPass)。它具有可移植性,并且可以抵抗多种常见的用户密码攻击。
jww 2014年

1
对于今天在这个问题上绊脚石的人们,必须“使用PDO而不是字符串插值”。
基金莫妮卡的诉讼

Answers:


270

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

由于安全性往往要比大多数程序员独自解决的复杂得多,并且具有更多看不见的破坏可能性,因此使用标准库几乎总是最简单,最安全(如果不是唯一的话)的可用选项。


新的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 / PepperedPasswordsgithub)。
它带有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';
}


OLD标准库

请注意:您不再需要此功能!这仅出于历史目的。

看一下:可移植的PHP密码哈希框架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';
}

PHPass已在一些众所周知的项目中实现:

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

好消息是,您不必担心细节,这些细节是由经验丰富的人编写的,并已由Internet上的许多人检查过。

有关密码存储方案的更多信息,请阅读Jeff的博客文章:您可能不正确地存储了密码

如果您采用“ 我会自己做,谢谢 ”的方法,无论您做什么,都不要使用MD5SHA1不再使用。它们是很好的哈希算法,但是出于安全考虑认为是无效的

当前,最好将CRYPT_BLOWFISH与crypt一起使用。
PHP中的CRYPT_BLOWFISH是Bcrypt哈希的实现。Bcrypt基于Blowfish块密码,利用其昂贵的密钥设置来减慢算法速度。


29

如果使用参数化查询而不是连接SQL语句,则用户将更加安全。和应该是每个用户唯一的,应与密码哈希一起存储。


1
Nettuts +上有一篇关于PHP安全性的好文章,还提到了密码盐化。也许你应该看看: net.tutsplus.com/tutorials/php/...
法比奥·安图内斯

3
Nettuts +是一个非常糟糕的文章,不能用作模型-它包括MD5的使用,即使加盐也可以很容易将其强行使用。相反,只需使用PHPass库,是远远比你可以找到一个教程网站的任何代码,也就是这样的回答:stackoverflow.com/questions/1581610/...
RichVel

11

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

含盐的好处在于,它使攻击者更难预先生成每个词典单词的MD5签名。但是,如果攻击者得知您有固定盐,那么他们可以预先生成以固定盐为前缀的每个词典单词的MD5签名。

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


3
盐通常与密码哈希(例如,crypt()函数的输出)一起存储。而且由于无论如何都必须检索密码哈希,因此使用用户特定的盐不会使该过程变得更加昂贵。(或者您是说生成新的随机盐很昂贵?我真的不这么认为。)否则+1。
Inshallah

为了安全起见,您可能只想通过存储过程提供对表的访问,并防止散列返回。而是,客户端传递它认为是哈希的内容,并获得成功或失败标志。这使得存储过程登录尝试,创建一个会话,等等
史蒂芬Sudit

@Inshallah-如果所有用户都使用相同的盐,则可以重复使用对user2的user1字典攻击。但是,如果每个用户都有一个唯一的盐,则需要为要攻击的每个用户生成一个新的词典。
R Samuel Klatchko 2010年

@R Samuel-这就是我投票赞成您的答案的原因,因为它建议了避免此类攻击的最佳实践策略。我的评论是要表达我对您所说的关于每用户盐的额外费用的困惑,我根本不了解。(由于“盐通常与密码哈希一起存储”,每位用户盐的任何其他存储和CPU要求都非常细微,以至于甚至不需要提及它们。)
Inshallah 2010年

@Inshallah-我正在考虑一种情况,您需要检查数据库是否可以使用哈希密码(然后您可以通过一次数据库检索来获取盐,而通过第二次数据库访问来检查哈希密码)。您可以通过一次检索下载盐/哈希密码,然后在客户端上进行比较的情况是正确的。对困惑感到抱歉。
R Samuel Klatchko 2010年

11

对于PHP 5.5(我所描述的甚至更早的版本也可用,请参见下文),我建议使用其新的内置解决方案:password_hash()password_verify()。为了提供所需的密码安全级别,它提供了几个选项(例如,通过$options数组指定“ cost”参数)

<?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文件),可以为您password_hash提供PHP 5.3.7+中的PHP 5.5 :https : //github.com/ircmaxell/password_compat


2
在大多数情况下,最好省略salt参数。该函数从操作系统的随机源创建盐,您很少有机会自己提供更好的盐。
martinstoeckli 2013年

1
那是我写的,不是吗?“如果未指定盐,则是随机生成的,因此,最好不指定盐”
akirk

大多数示例显示了如何添加两个参数,即使不建议添加盐也是如此,所以我想知道为什么吗?老实说,我只阅读代码后面的注释,而不是下一行。无论如何,当示例显示如何最好地使用该功能会更好吗?
martinstoeckli 2013年

你是对的,我同意。我相应地更改了答案,并注释了这一行。谢谢
akirk 2013年

我应该如何检查保存的密码和输入的密码是否相同?我正在使用password_hash()password_verify无论使用什么密码(无论正确与否),我最终都使用正确的密码
Brownman Revival

0

这对我来说没问题。Atwood先生写了MD5对彩虹桌的优势,基本上像坐着一样长的盐(虽然有些随机的标点/数字可能会改善它)。

您也可以看看SHA-1,这几天似乎越来越流行。


6
Atwood先生帖子底部的注释(红色)链接到安全从业人员的另一条帖子,该帖子指出使用MD5,SHA1和其他快速散列来存储密码是非常错误的。
sipwiz

2
@Matthew Scharley:我不同意昂贵的密码哈希算法带来的额外努力是虚假的安全性。这是为了防止容易猜到的密码被强行使用。如果要限制登录尝试,那么就可以防止发生相同的事情(尽管更为有效)。但是,如果对手可以访问存储在数据库中的哈希,则他将能够相当快地蛮力破解此类(容易猜出的)密码(取决于容易猜到的方式)。SHA-256 crypt算法的默认值为10000轮,这将使其难度增加10000倍。
Inshallah,2009年

3
慢速哈希实际上是通过多次迭代快速哈希,然后在每次迭代之间对数据进行混洗来实现的。目的是确保即使坏人获得了密码哈希的副本,他也必须消耗大量的CPU时间来针对您的哈希测试其字典。
caf

4
@caf:我相信bcrypt算法利用了Eksblowfish密钥调度的可参数化的昂贵性;不能完全确定它是如何工作的,但是在进行任何加密之前,密钥调度通常是在密码上下文对象初始化期间完成的非常昂贵的操作。
Inshallah,2009年

3
Inshallah:的确如此-bcrypt算法是一种不同的设计,其中底层的加密原语是分组密码而不是哈希函数。我指的是基于哈希函数的方案,例如PHK的MD5 crypt()。
caf

0

我要添加:

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

为了与旧系统兼容,通常会设置密码最大长度的限制。这是一个不好的安全策略:如果您设置限制,则仅针对密码的最小长度进行设置。

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

要恢复忘记的密码,您应该发送用户可以用来更改密码的地址。

  • 更新用户密码的哈希值

密码哈希可能已过期(算法参数可能已更新)。通过使用该功能,password_needs_rehash()您可以签出。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.