如何使用 PHP 的 password_hash 对密码进行散列和验证

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/30279321/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 21:41:05  来源:igfitidea点击:

How to use PHP's password_hash to hash and verify passwords

phpsaltpassword-hashphp-password-hash

提问by Josh Potter

Recently I have been trying to implement my own security on a log in script I stumbled upon on the internet. After struggling of trying to learn how to make my own script to generate a salt for each user, I stumbled upon password_hash.

最近,我一直在尝试在互联网上偶然发现的登录脚本上实现自己的安全性。在努力学习如何制作我自己的脚本来为每个用户生成盐之后,我偶然发现了password_hash.

From what I understand (based off of the reading on this page), salt is already generated in the row when you use password_hash. Is this true?

据我了解(基于本页的阅读),当您使用password_hash. 这是真的?

Another question I had was, wouldn't it be smart to have 2 salts? One directly in the file and one in the DB? That way, if someone compromises your salt in the DB, you still have the one directly in the file? I read on here that storing salts is never a smart idea, but it always confused me what people meant by that.

我的另一个问题是,有 2 种盐不是很聪明吗?一个直接在文件中,一个在数据库中?那样的话,如果有人在数据库中泄露了你的盐,你仍然直接在文件中拥有盐吗?我在这里读到,储存盐从来都不是一个聪明的主意,但它总是让我困惑人们的意思。

回答by Akar

Using password_hashis the recommended way to store passwords. Don't separate them to DB and files.

使用password_hash是推荐的存储密码的方式。不要将它们分开到数据库和文件。

Let's say we have the following input:

假设我们有以下输入:

$password = $_POST['password'];

I don't validate the input just for the sake of understanding the concept.

我不会仅仅为了理解这个概念而验证输入。

You first hash the password by doing this:

您首先通过执行以下操作来散列密码:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Then see the output:

然后查看输出:

var_dump($hashed_password);

As you can see it's hashed. (I assume you did those steps).

如您所见,它是经过哈希处理的。(我假设你做了这些步骤)。

Now you store this hashed_password in your database, ensuring your password column is large enough to hold the hashed value (at least 60 characters or longer). When a user asks to log them in, you check the password input with this hash value in the database, by doing this:

现在您将此 hashed_pa​​ssword 存储在您的数据库中,确保您的密码列足够大以保存散列值(至少 60 个字符或更长)。当用户要求登录时,您可以通过执行以下操作在数据库中使用此哈希值检查密码输入:

// Query the database for username and password
// ...

if(password_verify($password, $hashed_password)) {
    // If the password inputs matched the hashed password in the database
    // Do something, you know... log them in.
} 

// Else, Redirect them back to the login page.

Official Reference

官方参考

回答by martinstoeckli

Yes you understood it correctly, the function password_hash() will generate a salt on its own, and includes it in the resulting hash-value. Storing the salt in the database is absolutely correct, it does its job even if known.

是的,您理解正确,函数 password_hash() 将自行生成一个盐,并将其包含在生成的哈希值中。将盐存储在数据库中是绝对正确的,即使知道它也能完成它的工作。

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);

The second salt you mentioned (the one stored in a file), is actually a pepper or a server side key. If you add it before hashing (like the salt), then you add a pepper. There is a better way though, you could first calculate the hash, and afterwards encrypt (two-way) the hash with a server-side key. This gives you the possibility to change the key when necessary.

您提到的第二种盐(存储在文件中的盐)实际上是胡椒或服务器端密钥。如果你在散列之前添加它(比如盐),那么你就加了一个胡椒粉。不过,有更好的方法,您可以先计算散列,然后使用服务器端密钥加密(双向)散列。这使您可以在必要时更改密钥。

In contrast to the salt, this key should be kept secret. People often mix it up and try to hide the salt, but it is better to let the salt do its job and add the secret with a key.

与盐相反,这个密钥应该保密。人们经常将其混为一谈并试图隐藏盐,但最好让盐发挥作用并用密钥添加秘密。

回答by Joel Hinz

Yes, it's true. Why do you doubt the php faq on the function? :)

对,是真的。为什么怀疑php faq上的函数?:)

The result of running password_hash()has has four parts:

运行的结果password_hash()有四个部分:

  1. the algorithm used
  2. parameters
  3. salt
  4. actual password hash
  1. 使用的算法
  2. 参数
  3. 实际密码哈希

So as you can see, the hash is a part of it.

如您所见,散列是其中的一部分。

Sure, you could have an additional salt for an added layer of security, but I honestly think that's overkill in a regular php application. The default bcrypt algorithm is good, and the optional blowfish one is arguably even better.

当然,您可以为额外的安全层添加额外的盐,但老实说,我认为这在常规 php 应用程序中有点过头了。默认的 bcrypt 算法很好,可选的河豚算法可以说更好。

回答by Mahesh Yadav

Never use md5() for securing your password, even with salt, it is always dangerous!!

永远不要使用 md5() 来保护你的密码,即使是用盐,它总是很危险!!

Make your password secured with latest hashing algorithms as below.

使用最新的哈希算法保护您的密码,如下所示。

<?php

// Your original Password
$password = '121@121';

//PASSWORD_BCRYPT or PASSWORD_DEFAULT use any in the 2nd parameter
/*
PASSWORD_BCRYPT always results 60 characters long string.
PASSWORD_DEFAULT capacity is beyond 60 characters
*/
$password_encrypted = password_hash($password, PASSWORD_BCRYPT);

?>

For matching with database's encrypted password and user inputted password use the below function.

为了与数据库的加密密码和用户输入的密码匹配,请使用以下功能。

<?php 

if (password_verify($password_inputted_by_user, $password_encrypted)) {
    // Success!
    echo 'Password Matches';
}else {
    // Invalid credentials
    echo 'Password Mismatch';
}

?>

If you want to use your own salt, use your custom generated function for the same, just follow below, but I not recommend this as It is found deprecated in latest versions of PHP.

如果您想使用自己的盐,请使用自定义生成的函数,只需按照下面的操作,但我不推荐这样做,因为它在最新版本的 PHP 中已被弃用。

read this http://php.net/manual/en/function.password-hash.phpbefore use below code.

在使用下面的代码之前阅读这个http://php.net/manual/en/function.password-hash.php

<?php

$options = [
    'salt' => your_custom_function_for_salt(), 
    //write your own code to generate a suitable & secured salt
    'cost' => 12 // the default cost is 10
];

$hash = password_hash($your_password, PASSWORD_DEFAULT, $options);

?>

Hope these all helps!!

希望这些都有帮助!!

回答by Sammitch

There is a distinct lack of discussion on backwards and forwards compatibility that is built in to PHP's password functions. Notably:

关于内置于 PHP 密码函数的向后和向前兼容性的讨论明显缺乏。尤其:

  1. Backwards Compatibility:The password functions are essentially a well-written wrapper around crypt(), and are inherently backwards-compatible with crypt()-format hashes, even if they use obsolete and/or insecure hash algorithms.
  2. Forwards Compatibilty:Inserting password_needs_rehash()and a bit of logic into your authentication workflow can keep you your hashes up to date with current and future algorithms with potentially zero future changes to the workflow. Note: Any string that does not match the specified algorithm will be flagged for needing a rehash, including non-crypt-compatible hashes.
  1. 向后兼容性:密码函数本质上是围绕 的精心编写的包装器crypt(),并且本质上与crypt()-format 散列向后兼容,即使它们使用过时和/或不安全的散列算法。
  2. 向前兼容性:password_needs_rehash()您的身份验证工作流程中插入一些逻辑可以使您的哈希值与当前和未来的算法保持同步,并且未来对工作流程的更改可能为零。注意:任何与指定算法不匹配的字符串都将被标记为需要重新散列,包括非加密兼容的散列。

Eg:

例如:

class FakeDB {
    public function __call($name, $args) {
        printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
        return $this;
    }
}

class MyAuth {
    protected $dbh;
    protected $fakeUsers = [
        // old crypt-md5 format
        1 => ['password' => '$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => 'yeUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
    ];

    public function __construct($dbh) {
        $this->dbh = $dbh;
    }

    protected function getuser($id) {
        // just pretend these are coming from the DB
        return $this->fakeUsers[$id];
    }

    public function authUser($id, $password) {
        $userInfo = $this->getUser($id);

        // Do you have old, turbo-legacy, non-crypt hashes?
        if( strpos( $userInfo['password'], '$' ) !== 0 ) {
            printf("%s::legacy_hash\n", __METHOD__);
            $res = $userInfo['password'] === md5($password . $userInfo['salt']);
        } else {
            printf("%s::password_verify\n", __METHOD__);
            $res = password_verify($password, $userInfo['password']);
        }

        // once we've passed validation we can check if the hash needs updating.
        if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
            printf("%s::rehash\n", __METHOD__);
            $stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
            $stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
        }

        return $res;
    }
}

$auth = new MyAuth(new FakeDB());

for( $i=1; $i<=3; $i++) {
    var_dump($auth->authuser($i, 'foo'));
    echo PHP_EOL;
}

Output:

输出:

MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["y$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)

MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["y$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)

MyAuth::authUser::password_verify
bool(true)

As a final note, given that you can only re-hash a user's password on login you should consider "sunsetting" insecure legacy hashes to protect your users. By this I mean that after a certain grace period you remove all insecure [eg: bare MD5/SHA/otherwise weak] hashes and have your users rely on your application's password reset mechanisms.

最后要注意的是,鉴于您只能在登录时重新散列用户的密码,您应该考虑“取消”不安全的旧散列以保护您的用户。我的意思是,在一定的宽限期之后,您将删除所有不安全的 [例如:裸 MD5/SHA/其他弱] 哈希值,并使您的用户依赖于您的应用程序的密码重置机制。

回答by Dimitris Maniatis

Class Password full code:

班级密码全码:

Class Password {

    public function __construct() {}


    /**
     * Hash the password using the specified algorithm
     *
     * @param string $password The password to hash
     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
     * @param array  $options  The options for the algorithm to use
     *
     * @return string|false The hashed password, or false on error.
     */
    function password_hash($password, $algo, array $options = array()) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
            return null;
        }
        if (!is_string($password)) {
            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
            return null;
        }
        if (!is_int($algo)) {
            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
            return null;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
                $cost = 10;
                if (isset($options['cost'])) {
                    $cost = $options['cost'];
                    if ($cost < 4 || $cost > 31) {
                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
                        return null;
                    }
                }
                // The length of salt to generate
                $raw_salt_len = 16;
                // The length required in the final serialization
                $required_salt_len = 22;
                $hash_format = sprintf("y$%02d$", $cost);
                break;
            default :
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                return null;
        }
        if (isset($options['salt'])) {
            switch (gettype($options['salt'])) {
                case 'NULL' :
                case 'boolean' :
                case 'integer' :
                case 'double' :
                case 'string' :
                    $salt = (string)$options['salt'];
                    break;
                case 'object' :
                    if (method_exists($options['salt'], '__tostring')) {
                        $salt = (string)$options['salt'];
                        break;
                    }
                case 'array' :
                case 'resource' :
                default :
                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
                    return null;
            }
            if (strlen($salt) < $required_salt_len) {
                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
                return null;
            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
                $salt = str_replace('+', '.', base64_encode($salt));
            }
        } else {
            $salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
        }
        $salt = substr($salt, 0, $required_salt_len);

        $hash = $hash_format . $salt;

        $ret = crypt($password, $hash);

        if (!is_string($ret) || strlen($ret) <= 13) {
            return false;
        }

        return $ret;
    }


    /**
     * Generates Entropy using the safest available method, falling back to less preferred methods depending on support
     *
     * @param int $bytes
     *
     * @return string Returns raw bytes
     */
    function generate_entropy($bytes){
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = strlen($buffer);
            while ($read < $bytes) {
                $buffer .= fread($f, $bytes - $read);
                $read = strlen($buffer);
            }
            fclose($f);
            if ($read >= $bytes) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid || strlen($buffer) < $bytes) {
            $bl = strlen($buffer);
            for ($i = 0; $i < $bytes; $i++) {
                if ($i < $bl) {
                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
                } else {
                    $buffer .= chr(mt_rand(0, 255));
                }
            }
        }
        return $buffer;
    }

    /**
     * Get information about the password hash. Returns an array of the information
     * that was used to generate the password hash.
     *
     * array(
     *    'algo' => 1,
     *    'algoName' => 'bcrypt',
     *    'options' => array(
     *        'cost' => 10,
     *    ),
     * )
     *
     * @param string $hash The password hash to extract info from
     *
     * @return array The array of information about the hash.
     */
    function password_get_info($hash) {
        $return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
        if (substr($hash, 0, 4) == 'y$' && strlen($hash) == 60) {
            $return['algo'] = PASSWORD_BCRYPT;
            $return['algoName'] = 'bcrypt';
            list($cost) = sscanf($hash, "y$%d$");
            $return['options']['cost'] = $cost;
        }
        return $return;
    }

    /**
     * Determine if the password hash needs to be rehashed according to the options provided
     *
     * If the answer is true, after validating the password using password_verify, rehash it.
     *
     * @param string $hash    The hash to test
     * @param int    $algo    The algorithm used for new password hashes
     * @param array  $options The options array passed to password_hash
     *
     * @return boolean True if the password needs to be rehashed.
     */
    function password_needs_rehash($hash, $algo, array $options = array()) {
        $info = password_get_info($hash);
        if ($info['algo'] != $algo) {
            return true;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                $cost = isset($options['cost']) ? $options['cost'] : 10;
                if ($cost != $info['options']['cost']) {
                    return true;
                }
                break;
        }
        return false;
    }

    /**
     * Verify a password against a hash using a timing attack resistant approach
     *
     * @param string $password The password to verify
     * @param string $hash     The hash to verify against
     *
     * @return boolean If the password matches the hash
     */
    public function password_verify($password, $hash) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
            return false;
        }
        $ret = crypt($password, $hash);
        if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
            return false;
        }

        $status = 0;
        for ($i = 0; $i < strlen($ret); $i++) {
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
        }

        return $status === 0;
    }

}

回答by Gerrit Fries

I've built a function I use all the time for password validation and to create passwords, e.g. to store them in a MySQL database. It uses a randomly generated salt which is way more secure than using a static salt.

我已经构建了一个一直用于密码验证和创建密码的函数,例如将它们存储在 MySQL 数据库中。它使用随机生成的盐,这比使用静态盐更安全。

function secure_password($user_pwd, $multi) {

/*
    secure_password ( string $user_pwd, boolean/string $multi ) 

    *** Description: 
        This function verifies a password against a (database-) stored password's hash or
        returns $hash for a given password if $multi is set to either true or false

    *** Examples:
        // To check a password against its hash
        if(secure_password($user_password, $row['user_password'])) {
            login_function();
        } 
        // To create a password-hash
        $my_password = 'uber_sEcUrE_pass';
        $hash = secure_password($my_password, true);
        echo $hash;
*/

// Set options for encryption and build unique random hash
$crypt_options = ['cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)];
$hash = password_hash($user_pwd, PASSWORD_BCRYPT, $crypt_options);

// If $multi is not boolean check password and return validation state true/false
if($multi!==true && $multi!==false) {
    if (password_verify($user_pwd, $table_pwd = $multi)) {
        return true; // valid password
    } else {
        return false; // invalid password
    }
// If $multi is boolean return $hash
} else return $hash;

}