今天,我将向您展示如何在 PHP 中对密码进行哈希处理。
在本分步教程中,您将学习:
所以,如果你想学习如何在 PHP 中加密密码,这就是你的教程。
让我们潜入水中。
正在寻找完整的登录和身份验证教程?
这是:PHP登录和身份验证教程
作为 PHP 开发人员,您必须知道如何安全地存储密码。
出于以下原因:
那么,让我们从这个问题开始:
(简短回答:否)
过去,密码是使用 MD5 或 SHA1 哈希存储的。
像这样:
-
- /* User's password. */
- $password = 'my secret password';
-
- /* MD5 hash to be saved in the database. */
- $hash = md5($password);
然而,这种技术不够安全。
有两个原因:
如果攻击者窃取了 MD5 或 SHA 哈希,他或她也可以轻松找到原始密码。
换句话说,这些哈希值几乎和纯文本密码一样不安全。
解决方案是使用安全散列函数:password_hash()。
让我们看看它是如何工作的。
password_hash()函数创建密码的安全哈希。
这是您可以使用它的方式:
-
- /* User's password. */
- $password = 'my secret password';
-
- /* Secure password hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT);
password_hash()的结果哈希是安全的,因为:
获得密码哈希后,您可以将其直接保存在数据库中。
让我们看看接下来的例子。
首先,您需要一个数据库用户表。
例如,让我们使用我的Authentication Tutorial中的“accounts”表的简化版本。
此表具有以下列:
这是创建表的 SQL 代码(您可以将它与 PhpMyAdmin 一起使用以在您的开发环境中创建表):
-
- CREATE TABLE `accounts` (
- `account_id` int(10) UNSIGNED NOT NULL,
- `account_name` varchar(255) NOT NULL,
- `account_passwd` varchar(255) NOT NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
- ALTER TABLE `accounts`
- ADD PRIMARY KEY (`account_id`);
-
- ALTER TABLE `accounts`
- MODIFY `account_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
重要的:
请务必将密码列设置为 varchar 。
(varchar 是可变长度的文本列。)
原因是 password_hash()的散列大小可以改变(稍后会详细介绍)。
如果您需要 SQL 方面的帮助,可以在这里找到所需的一切:如何将 PHP 与 MySQL 一起使用
现在,您需要从 PHP 脚本连接到数据库。
如果您不知道如何操作,这里有一个简单的 PDO 连接脚本,您可以立即使用。
只需编辑连接参数以使其适用于您自己的环境:
- /* Host name of the MySQL server. */
- $host = 'localhost';
-
- /* MySQL account username. */
- $user = 'myUser';
-
- /* MySQL account password. */
- $passwd = 'myPasswd';
-
- /* The default schema you want to use. */
- $schema = 'mySchema';
-
- /* The PDO object. */
- $pdo = NULL;
-
- /* Connection string, or "data source name". */
- $dsn = 'mysql:host=' . $host . ';dbname=' . $schema;
-
- /* Connection inside a try/catch block. */
- try
- {
- /* PDO object creation. */
- $pdo = new PDO($dsn, $user, $passwd);
-
- /* Enable exceptions on errors. */
- $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
- }
- catch (PDOException $e)
- {
- /* If there is an error, an exception is thrown. */
- echo 'Database connection failed.';
- die();
- }
现在您已准备好向表中添加新用户。
这是一个完整的示例(pdo.php是包含先前数据库连接片段的脚本):
-
- /* Include the database connection script. */
- include 'pdo.php';
-
- /* Username. */
- $username = 'John';
-
- /* Password. */
- $password = 'my secret password';
-
- /* Secure password hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT);
-
- /* Insert query template. */
- $query = 'INSERT INTO accounts (account_name, account_passwd) VALUES (:name, :passwd)';
-
- /* Values array for PDO. */
- $values = [':name' => $username, ':passwd' => $hash];
-
- /* Execute the query. */
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
-
- /* New password. */
- $password = $_POST['password'];
-
- /* Remember to validate the password. */
-
- /* Create the new password hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT);
然后,更新与当前用户具有相同账户ID的表行,并设置新的哈希值。
注意:我们假设$accountId变量包含帐户 ID。
-
- /* Include the database connection script. */
- include 'pdo.php';
-
- /* ID of the account to edit. */
- $accountId = 1;
-
- /* Update query template. */
- $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
-
- /* Values array for PDO. */
- $values = [':passwd' => $hash, ':id' => $accountId];
-
- /* Execute the query. */
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
要验证远程用户提供的密码,您需要使用password_verify()函数。
password_verify()有两个参数:
如果密码正确,password_verify()返回true。
这是一个例子:
- /* Include the database connection script. */
- include 'pdo.php';
-
- /* Login status: false = not authenticated, true = authenticated. */
- $login = FALSE;
-
- /* Username from the login form. */
- $username = $_POST['username'];
-
- /* Password from the login form. */
- $password = $_POST['password'];
-
- /* Remember to validate $username and $password. */
-
- /* Look for the username in the database. */
- $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
-
- /* Values array for PDO. */
- $values = [':name' => $username];
-
- /* Execute the query */
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
-
- $row = $res->fetch(PDO::FETCH_ASSOC);
-
- /* If there is a result, check if the password matches using password_verify(). */
- if (is_array($row))
- {
- if (password_verify($password, $row['account_passwd']))
- {
- /* The password is correct. */
- $login = TRUE;
- }
- }
重要的:
您不能只比较两个不同的哈希值来查看它们是否匹配。
原因是password_hash()创建了salted hashes。
加盐哈希包括一个名为“salt”的随机字符串,作为对彩虹表和字典攻击的保护。
因此,即使源密码相同,每个哈希值也会不同。
试试下面的代码。即使密码相同,您也会看到两个哈希值不同:
-
- $password = 'my password';
-
- echo password_hash($password, PASSWORD_DEFAULT);
- echo '
'; - echo password_hash($password, PASSWORD_DEFAULT);
笔记:
password_verify() 仅适用于由password_hash()创建的哈希。
您不能使用它根据 MD5 或 SHA 哈希检查密码。
password_hash()生成的哈希是非常安全的。
但是您可以通过两种简单的技术使其更加强大:
Bcrypt是password_hash()使用的当前默认散列算法。
该算法采用一个名为“cost”的选项参数。默认成本值为 10。
通过增加成本,您可以使哈希更难以计算。成本越高,创建哈希所需的时间就越长。
较高的成本使破解散列变得更加困难。但是,它也使哈希创建和检查时间更长。
因此,您希望在安全性和服务器负载之间找到折衷方案。
这是您可以为password_hash()设置自定义成本值的方法:
-
- /* Password. */
- $password = 'my secret password';
-
- /* Set the "cost" parameter to 12. */
- $options = ['cost' => 12];
-
- /* Create the hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT, $options);
但是你应该设置什么成本值?
一个好的折衷方案是让您的服务器在大约 100 毫秒内创建散列的成本值。
这是一个简单的测试来找到这个值:
-
- /* 100 ms. */
- $time = 0.1;
-
- /* Initial cost. */
- $cost = 10;
-
- /* Loop until the time required is more than 100ms. */
- do
- {
- /* Increase the cost. */
- $cost++;
-
- /* Check how much time we need to create the hash. */
- $start = microtime(true);
- password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
- $end = microtime(true);
- }
- while (($end - $start) < $time);
-
- echo 'Cost found: ' . $cost;
找到成本后,每次执行password_hash()时都可以使用它,就像前面的示例一样。
为了理解这一步,让我们看看 password_hash()是如何 工作的。
password_hash()接受三个参数:
PHP 支持不同的散列算法,但您通常希望使用默认的一种。
您可以使用PASSWORD_DEFAULT常量来选择默认算法,正如您在前面的示例中看到的那样。
截至 2020 年 6 月,默认算法为 Bcrypt。
但是,如果实现了更好、更安全的算法,PHP 将来可以更改默认算法。
发生这种情况时, PASSWORD_DEFAULT 常量将指向新算法。因此,所有新的哈希都将使用新算法创建。
但是,如果您想使用以前的算法生成的所有旧哈希,并使用新的自动重新创建它们怎么办?
这就是password_needs_rehash()发挥作用的地方。
此函数检查是否已使用给定的算法和参数创建了哈希。
例如:
- /* Password. */
- $password = 'my secret password';
-
- /* Set the "cost" parameter to 10. */
- $options = ['cost' => 10];
-
- /* Create the hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT, $options);
-
- /* Now, change the cost. */
- $options['cost'] = 12;
-
- /* Check if the hash needs to be created again. */
- if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
- {
- echo 'You need to rehash the password.';
- }
如果当前默认散列算法与用于创建散列的算法不同,则 password_needs_rehash()返回true。
password_needs_rehash() 还检查选项参数 是否不同。
如果您想在更改 Bcrypt 成本等参数后更新哈希值,这将非常方便。
此示例显示了如何在远程用户登录时自动检查密码哈希并在需要时对其进行更新:
- /* Include the database connection script. */
- include 'pdo.php';
-
- /* Set the "cost" parameter to 12. */
- $options = ['cost' => 12];
-
- /* Login status: false = not authenticated, true = authenticated. */
- $login = FALSE;
-
- /* Username from the login form. */
- $username = $_POST['username'];
-
- /* Password from the login form. */
- $password = $_POST['password'];
-
- /* Remember to validate $username and $password. */
-
- /* Look for the username in the database. */
- $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
-
- /* Values array for PDO. */
- $values = [':name' => $username];
-
- /* Execute the query */
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
-
- $row = $res->fetch(PDO::FETCH_ASSOC);
-
- /* If there is a result, check if the password matches using password_verify(). */
- if (is_array($row))
- {
- if (password_verify($password, $row['account_passwd']))
- {
- /* The password is correct. */
- $login = TRUE;
-
- /* Check if the hash needs to be created again. */
- if (password_needs_rehash($row['account_passwd'], PASSWORD_DEFAULT, $options))
- {
- $hash = password_hash($password, PASSWORD_DEFAULT, $options);
-
- /* Update the password hash on the database. */
- $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
- $values = [':passwd' => $hash, ':id' => $row['account_id']];
-
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
- }
- }
- }
在此示例中,您将实现一个简单的脚本来自动将旧的、基于 MD5 的哈希转换为使用password_hash()创建的安全哈希。
这是它的工作原理:
这是脚本:
- /* Include the database connection script. */
- include 'pdo.php';
-
- /* Set the "cost" parameter to 12. */
- $options = ['cost' => 12];
-
- /* Login status: false = not authenticated, true = authenticated. */
- $login = FALSE;
-
- /* Username from the login form. */
- $username = $_POST['username'];
-
- /* Password from the login form. */
- $password = $_POST['password'];
-
- /* Remember to validate $username and $password. */
-
- /* Look for the username in the database. */
- $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
-
- /* Values array for PDO. */
- $values = [':name' => $username];
-
- /* Execute the query */
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
-
- $row = $res->fetch(PDO::FETCH_ASSOC);
-
- /* If there is a result, check if the password matches using password_verify(). */
- if (is_array($row))
- {
- if (password_verify($password, $row['account_passwd']))
- {
- /* The password is correct. */
- $login = TRUE;
-
- /* You can also use password_needs_rehash() here, as shown in the previous example. */
- }
- else
- {
- /* Check if the database contains the MD5 hash of the password. */
- if (md5($password) == $row['account_passwd'])
- {
- /* The password is correct. */
- $login = TRUE;
-
- /* Update the database with a new, secure hash. */
- $hash = password_hash($password, PASSWORD_DEFAULT, $options);
- $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
- $values = [':passwd' => $hash, ':id' => $row['account_id']];
-
- try
- {
- $res = $pdo->prepare($query);
- $res->execute($values);
- }
- catch (PDOException $e)
- {
- /* Query error. */
- echo 'Query error.';
- die();
- }
- }
- }
- }
在本教程中,您学习了如何使用password_hash()和password_verify()来创建密码的安全散列(以及为什么不应该使用 MD5)。
您还学习了如何通过设置适当的Bcrypt 成本并根据需要自动重新散列密码来使密码散列更加安全。
现在轮到你了:在下面留下你的问题和评论。