【问题标题】:How to use PHP's password_hash to hash and verify passwords如何使用 PHP 的 password_hash 来散列和验证密码
【发布时间】:2022-01-09 12:40:52
【问题描述】:

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

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

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

【问题讨论】:

  • 没有。让函数处理盐。双重腌制会给你带来麻烦,没有必要。

标签: php salt password-hash php-password-hash


【解决方案1】:

使用password_hash 是存储密码的推荐方式。不要将它们分离到 DB 和文件中。

假设我们有以下输入:

$password = $_POST['password'];

您首先通过这样做来散列密码:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

然后看输出:

var_dump($hashed_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.

Official Reference

【讨论】:

  • 好的,我刚试过,它成功了。我怀疑这个功能,因为它看起来太简单了。您建议我将 varchar 的长度设为多长时间? 225?
  • 这已经在手册中 php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php OP 可能没有阅读或理解。这个问题被问到的次数比没有人多。
  • @FunkFortyNiner,b/c Josh 提出了这个问题,我在 2 年后找到了这个问题,它帮助了我。这就是 SO 的意义所在。那本手册就像泥巴一样清晰。
  • 至于长度,来自关于password_hash的PHP手册,示例中有一条评论——“注意DEFAULT可能会随着时间的推移而改变,所以你需要通过允许你的存储扩展过去来做准备60 个字符(最好是 255 个)"
  • @toddmo :为了支持您的评论,我刚刚在 2020 年 6 月提出了这个问题,讨论为我节省了数小时的挫败感。我也发现 PHP 手册大部分时间都像泥巴一样清晰。
【解决方案2】:

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

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

与盐相比,此密钥应保密。人们经常把它混在一起,试图隐藏盐,但最好让盐发挥作用,用钥匙添加秘密。

【讨论】:

    【解决方案3】:

    永远不要使用 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';
    }
    

    如果您想使用自己的 salt,请使用您自定义生成的函数,只需按照以下说明进行操作,但我不推荐这样做,因为它在最新版本的 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);
    

    【讨论】:

    • 不推荐使用 salt 选项有充分的理由,因为该函数会尽力生成加密安全的 salt,而且几乎不可能做得更好。
    • 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。我想使用密码哈希..
    • PASSWORD_DEFAULT - 使用 bcrypt 算法(需要 PHP 5.5.0)。请注意,此常量旨在随着 PHP 中添加新的和更强大的算法而随时间而变化。因此,使用此标识符的结果长度可能会随时间而变化。
    【解决方案4】:

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

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

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

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

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

    【讨论】:

    • BCrypt 是一个散列函数,而 Blowfish 是一个加密算法。 BCrypt 起源于 Blowfish 算法。
    【解决方案5】:

    对于内置于 PHP 密码函数的向后和向前兼容性的讨论明显不足。值得注意的是:

    1. 向后兼容性:密码函数本质上是对crypt() 的精心编写的包装,并且本质上向后兼容crypt() 格式的哈希,即使它们使用过时和/或不安全的哈希算法.
    2. 转发兼容性:在您的身份验证工作流程中插入password_needs_rehash() 和一些逻辑可以使您的哈希值与当前和未来的算法保持同步,并且未来可能对工作流程进行零更改。注意:任何与指定算法不匹配的字符串都将被标记为需要重新哈希,包括不兼容加密的哈希。

    例如:

    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/否则很弱] 哈希,并让您的用户依赖您的应用程序的密码重置机制。

    【讨论】:

    • 是的。当我将密码安全性更改为使用password_hash 时,我故意使用了较低的cost 值,因此我可以稍后增加它并检查password_needs_rehash() 是否按预期工作。 (cost 较低的版本从未投入生产。)
    【解决方案6】:

    班级密码完整代码:

    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;
        }
    
    }
    

    【讨论】:

      【解决方案7】:

      我已经构建了一个我一直使用的函数来验证密码和创建密码,例如将它们存储在 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;
      
      }
      

      【讨论】:

      • 最好省略salt参数,它将由password_hash()函数自动生成,遵循最佳实践。可以使用PASSWORD_DEFAULT 代替PASSWORD_BCRYPT 来编写未来证明代码。
      • 根据secure.php.net/manual/en/function.password-hash.php "从 PHP 7.0.0 起,salt 选项已被弃用。现在更倾向于简单地使用默认生成的 salt。"
      猜你喜欢
      • 2015-06-03
      • 2014-12-03
      • 2019-02-15
      • 1970-01-01
      • 2015-03-10
      相关资源
      最近更新 更多