如何使用PHP的password_hash哈希和验证密码


93

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

据我了解(基于本页的阅读内容),当您使用时,盐已经在行中生成password_hash。这是真的?

我的另一个问题是,吃2种盐不是很聪明吗?一个直接在文件中,另一个在数据库中?这样,如果有人破坏了数据库中的盐,您仍然直接在文件中保留了盐吗?我在这里读到,存储盐从来都不是一个聪明的主意,但是它总是让我感到困惑。


8
否。请让该功能处理盐。两次加盐都会给您带来麻烦,因此没有必要。
Funk 40 Niner,2015年

Answers:


181

password_hash建议使用来存储密码。不要将它们分离为数据库和文件。

假设我们有以下输入:

$password = $_POST['password'];

您首先通过执行以下操作对密码进行哈希处理:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

然后查看输出:

var_dump($hashed_password);

如您所见,它是散列的。(我假设您已执行这些步骤)。

现在,您将此哈希密码存储在数据库中,确保您的password列足够大以容纳哈希值(至少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.

官方参考


2
好的,我只是尝试了一下,它奏效了。我对此功能表示怀疑,因为它似乎太简单了。您建议我使用varchar长度多长时间?225?
乔什·波特

4
这已经在手册php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php中,OP可能没有阅读或理解。这个问题被问得比没有问多。
Funk 40 Niner,2015年

那是另一页。
乔什·波特

@JoshPotter有什么不同?另外,请注意他们尚未回答您的第二个问题。他们可能期望您发现自己,或者他们不知道。
Funk 40 Niner,2015年

7
@ FunkFortyNiner,b / c Josh提出了这个问题,两年后我找到了这个问题,它对我有所帮助。这就是SO的重点。那本手册就像泥一样清晰。
toddmo '18

23

是的,您正确理解了它,函数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);

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

与盐相反,此密钥应保密。人们经常将其混合在一起并试图隐藏盐,但是最好让盐完成其工作并添加密钥。


8

对,是真的。为什么您对函数的php常见疑问表示怀疑?:)

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

  1. 使用的算法
  2. 参数
  3. 实际密码哈希

如您所见,哈希是其中的一部分。

当然,您可以在安全性方面加点盐,但是老实说,在常规的php应用程序中,这太过分了。默认的bcrypt算法是好的,可以说可选的河豚甚至更好。


2
BCrypt是一种哈希函数,而Blowfish是一种加密算法。BCrypt起源于Blowfish算法。
martinstoeckli

7

切勿使用md5()来保护密码,即使加盐也很危险!!

使用以下最新的哈希算法确保您的密码安全。

<?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);

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

<?php 

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

如果您想使用自己的盐,请按照以下步骤使用您自定义的生成函数,但是,我不建议您这样做,因为它在最新版本的PHP中已被弃用。

在使用以下代码之前,请阅读有关password_hash()的信息

<?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);

4
不建议使用salt选项,这是有充分理由的,因为该函数会尽力生成加密安全的salt,并且几乎不可能做得更好。
martinstoeckli

@martinstoeckli,是的,您是对的,我刚刚更新了答案,谢谢!
Mahesh Yadav

if(isset($ _ POST ['btn-signup'])){$ uname = mysql_real_escape_string($ _ POST ['uname']); $ email = mysql_real_escape_string($ _ POST ['email']); $ upass = md5(mysql_real_escape_string($ _ POST ['pass'])); 这是在login.php中使用的代码。我想在不使用转义和md5的情况下执行此操作。我想使用密码哈希..
rashmi SM

PASSWORD_DEFAULT-使用bcrypt算法(需要PHP 5.5.0)。请注意,该常数旨在随着时间的推移而变化,因为新的和更强大的算法已添加到PHP中。因此,使用此标识符的结果的长度可能会随时间变化。
Adrian P.

5

内置在PHP密码功能中的关于向后和向前兼容性的讨论非常缺乏。值得注意的是:

  1. 向后兼容性:密码功能本质上是一个写得很好的包装程序crypt(),并且crypt()即使使用过时和/或不安全的哈希算法,也固有地与-format哈希向后兼容。
  2. 转发兼容性:password_needs_rehash()身份验证工作流程中插入一些逻辑,可以使您的哈希值与当前和将来的算法保持最新​​,并且将来对工作流程的更改可能为零。注意:任何与指定算法不匹配的字符串都将被标记为需要重新哈希,包括不兼容crypt的哈希。

例如:

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' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => '$2y$10$3eUn9Rnf04DR.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;
}

输出:

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

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

MyAuth::authUser::password_verify
bool(true)

最后一点,鉴于您只能在登录时重新散列用户密码,因此应考虑“清除”不安全的旧式哈希值以保护用户。我的意思是,在一定的宽限期后,您将删除所有不安全的[例如:裸MD5 / SHA /否则为弱]哈希,并让用户依赖应用程序的密码重置机制。


0

类密码的完整代码:

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("$2y$%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) == '$2y$' && strlen($hash) == 60) {
            $return['algo'] = PASSWORD_BCRYPT;
            $return['algoName'] = 'bcrypt';
            list($cost) = sscanf($hash, "$2y$%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;
    }

}

0

我已经建立了一个函数,该函数一直用于密码验证和创建密码,例如将其存储在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;

}

6
最好省略该salt参数,它将按照最佳实践由password_hash()函数自动生成。而不是PASSWORD_BCRYPT一个人可以PASSWORD_DEFAULT用来编写将来的证明代码。
martinstoeckli

感谢您的建议。我必须在文档中进行了监督。过了很长的夜晚。
Gerrit Fries

1
根据secure.php.net/manual/en/function.password-hash.php的描述: “自PHP 7.0.0起,不推荐使用salt选项。现在,最好使用默认情况下生成的salt。”
jmng
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.