• 如何在 PHP 中对密码进行哈希处理


    今天,我将向您展示如何在 PHP 中对密码进行哈希处理

    在本分步教程中,您将学习:

    • 为什么像 MD5 这样的哈希不安全
    • 如何使用password_hash()创建安全密码哈希
    • 如何使用password_verify()验证密码
    • 提高哈希安全性的 2 种方法
    • 奖励教程:如何自动转换旧哈希

    所以,如果你想学习如何在 PHP 中加密密码,这就是你的教程。

    让我们潜入水中。

    内容

    正在寻找完整的登录和身份验证教程?

    这是:PHP登录和身份验证教程

    PHP密码加密安全

    作为 PHP 开发人员,您必须知道如何安全地存储密码

    出于以下原因:

    • 许多网络攻击旨在窃取您的用户密码。
      如果密码被盗,您必须确保攻击者无法解密它们。
    • 当今严格的隐私法规要求保护敏感数据,例如密码。不遵守可能会导致罚款。
    • 密码安全是您的客户期望您提供的基本PHP 安全功能之一。
      如果你学会了如何正确地做到这一点,你的声誉就会提高,你的客户也会信任你。

     

    那么,让我们从这个问题开始: 

    MD5 和 SHA 哈希是否安全?

    (简短回答:否)

    过去,密码是使用 MD5 或 SHA1 哈希存储的。

    像这样:

    1. /* User's password. */
    2. $password = 'my secret password';
    3. /* MD5 hash to be saved in the database. */
    4. $hash = md5($password);

    然而,这种技术不够安全。

    有两个原因:

    1. MD5 和 SHA 算法对于今天的计算能力来说太弱了。
    2. 简单的、未加盐的哈希很容易受到“彩虹表”和字典攻击。

     

    如果攻击者窃取了 MD5 或 SHA 哈希,他或她也可以轻松找到原始密码。

    换句话说,这些哈希值几乎和纯文本密码一样不安全。

    解决方案是使用安全散列函数:password_hash()

    让我们看看它是如何工作的。

     

    密码哈希()

    password_hash()函数创建密码的安全哈希。

    这是您可以使用它的方式:

    1. /* User's password. */
    2. $password = 'my secret password';
    3. /* Secure password hash. */
    4. $hash = password_hash($password, PASSWORD_DEFAULT);

     

    password_hash()的结果哈希是安全的,因为:

    • 它使用强大的散列算法
    • 它添加了随机盐以防止彩虹表和字典攻击。

    获得密码哈希后,您可以将其直接保存在数据库中。

    让我们看看接下来的例子。

    如何使用 password_hash()

    首先,您需要一个数据库用户表。

    例如,让我们使用我的Authentication Tutorial中的“accounts”表的简化版本。

    此表具有以下列:

    • account_id:帐户的唯一标识符。
    • account_name:帐户用户名。
    • account_passwd:密码哈希。

    这是创建表的 SQL 代码(您可以将它与 PhpMyAdmin 一起使用以在您的开发环境中创建表):

    1. CREATE TABLE `accounts` (
    2. `account_id` int(10) UNSIGNED NOT NULL,
    3. `account_name` varchar(255) NOT NULL,
    4. `account_passwd` varchar(255) NOT NULL
    5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    6. ALTER TABLE `accounts`
    7. ADD PRIMARY KEY (`account_id`);
    8. ALTER TABLE `accounts`
    9. MODIFY `account_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;

    重要的:

    请务必将密码列设置为 varchar 

    (varchar 是可变长度的文本列。)

    原因是 password_hash()的散列大小可以改变(稍后会详细介绍)。

    如果您需要 SQL 方面的帮助,可以在这里找到所需的一切:如何将 PHP 与 MySQL 一起使用

     

    现在,您需要从 PHP 脚本连接到数据库。

    如果您不知道如何操作,这里有一个简单的 PDO 连接脚本,您可以立即使用。

    只需编辑连接参数以使其适用于您自己的环境:

    1. /* Host name of the MySQL server. */
    2. $host = 'localhost';
    3. /* MySQL account username. */
    4. $user = 'myUser';
    5. /* MySQL account password. */
    6. $passwd = 'myPasswd';
    7. /* The default schema you want to use. */
    8. $schema = 'mySchema';
    9. /* The PDO object. */
    10. $pdo = NULL;
    11. /* Connection string, or "data source name". */
    12. $dsn = 'mysql:host=' . $host . ';dbname=' . $schema;
    13. /* Connection inside a try/catch block. */
    14. try
    15. {
    16. /* PDO object creation. */
    17. $pdo = new PDO($dsn, $user, $passwd);
    18. /* Enable exceptions on errors. */
    19. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    20. }
    21. catch (PDOException $e)
    22. {
    23. /* If there is an error, an exception is thrown. */
    24. echo 'Database connection failed.';
    25. die();
    26. }

     

    现在您已准备好向表中添加新用户。

    这是一个完整的示例(pdo.php是包含先前数据库连接片段的脚本):

    1. /* Include the database connection script. */
    2. include 'pdo.php';
    3. /* Username. */
    4. $username = 'John';
    5. /* Password. */
    6. $password = 'my secret password';
    7. /* Secure password hash. */
    8. $hash = password_hash($password, PASSWORD_DEFAULT);
    9. /* Insert query template. */
    10. $query = 'INSERT INTO accounts (account_name, account_passwd) VALUES (:name, :passwd)';
    11. /* Values array for PDO. */
    12. $values = [':name' => $username, ':passwd' => $hash];
    13. /* Execute the query. */
    14. try
    15. {
    16. $res = $pdo->prepare($query);
    17. $res->execute($values);
    18. }
    19. catch (PDOException $e)
    20. {
    21. /* Query error. */
    22. echo 'Query error.';
    23. die();
    24. }
    重要的:

    在此示例中,我们跳过了验证步骤,包括:

    • 检查用户名和密码长度
    • 检查无效字符
    • 检查用户名是否已经存在

    等等。

    验证超出了本教程的范围,但请记住,您始终需要验证输入变量。

    您可以参考我的登录和身份验证教程以获取更多详细信息和示例。

    如果您想了解有关 PHP 安全性的更多信息,请查看我的PHP 安全性课程

     

    如何更改用户的密码

    下一个示例显示如何更改现有用户的密码。

    首先,获取新密码并使用password_hash()创建其哈希:

    1. /* New password. */
    2. $password = $_POST['password'];
    3. /* Remember to validate the password. */
    4. /* Create the new password hash. */
    5. $hash = password_hash($password, PASSWORD_DEFAULT);

    然后,更新与当前用户具有相同账户ID的表行,并设置新的哈希值。

    注意:我们假设$accountId变量包含帐户 ID。

    1. /* Include the database connection script. */
    2. include 'pdo.php';
    3. /* ID of the account to edit. */
    4. $accountId = 1;
    5. /* Update query template. */
    6. $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
    7. /* Values array for PDO. */
    8. $values = [':passwd' => $hash, ':id' => $accountId];
    9. /* Execute the query. */
    10. try
    11. {
    12. $res = $pdo->prepare($query);
    13. $res->execute($values);
    14. }
    15. catch (PDOException $e)
    16. {
    17. /* Query error. */
    18. echo 'Query error.';
    19. die();
    20. }

    如何使用密码验证()

    要验证远程用户提供的密码,您需要使用password_verify()函数。

    password_verify()有两个参数:

    • 您需要验证的密码,作为第一个参数
    • 来自原始密码的password_hash()的哈希值,作为第二个参数

    如果密码正确,password_verify()返回true

    这是一个例子:

    1. /* Include the database connection script. */
    2. include 'pdo.php';
    3. /* Login status: false = not authenticated, true = authenticated. */
    4. $login = FALSE;
    5. /* Username from the login form. */
    6. $username = $_POST['username'];
    7. /* Password from the login form. */
    8. $password = $_POST['password'];
    9. /* Remember to validate $username and $password. */
    10. /* Look for the username in the database. */
    11. $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
    12. /* Values array for PDO. */
    13. $values = [':name' => $username];
    14. /* Execute the query */
    15. try
    16. {
    17. $res = $pdo->prepare($query);
    18. $res->execute($values);
    19. }
    20. catch (PDOException $e)
    21. {
    22. /* Query error. */
    23. echo 'Query error.';
    24. die();
    25. }
    26. $row = $res->fetch(PDO::FETCH_ASSOC);
    27. /* If there is a result, check if the password matches using password_verify(). */
    28. if (is_array($row))
    29. {
    30. if (password_verify($password, $row['account_passwd']))
    31. {
    32. /* The password is correct. */
    33. $login = TRUE;
    34. }
    35. }

     

    重要的:

    您不能只比较两个不同的哈希值来查看它们是否匹配。

    原因是password_hash()创建了salted hashes

    加盐哈希包括一个名为“salt”的随机字符串,作为对彩虹表和字典攻击的保护。

    因此,即使源密码相同,每个哈希值也会不同。

     

    试试下面的代码。即使密码相同,您也会看到两个哈希值不同:

    1. $password = 'my password';
    2. echo password_hash($password, PASSWORD_DEFAULT);
    3. echo '
      ';
    4. echo password_hash($password, PASSWORD_DEFAULT);

     

    笔记:

    password_verify() 仅适用于由password_hash()创建的哈希。

    您不能使用它根据 MD5 或 SHA 哈希检查密码。

    如何提高哈希安全性

    password_hash()生成的哈希是非常安全的。

    但是您可以通过两种简单的技术使其更加强大:

    1. 增加Bcrypt 成本
    2. 自动更新散列算法

     

    Bcrypt 费用

    Bcrypt是password_hash()使用的当前默认散列算法

    该算法采用一个名为“cost”的选项参数。默认成本值为 10。

    通过增加成本,您可以使哈希更难以计算。成本越高,创建哈希所需的时间就越长。

    较高的成本使破解散列变得更加困难。但是,它也使哈希创建和检查时间更长。

    因此,您希望在安全性和服务器负载之间找到折衷方案。

    这是您可以为password_hash()设置自定义成本值的方法:

    1. /* Password. */
    2. $password = 'my secret password';
    3. /* Set the "cost" parameter to 12. */
    4. $options = ['cost' => 12];
    5. /* Create the hash. */
    6. $hash = password_hash($password, PASSWORD_DEFAULT, $options);

     

    但是你应该设置什么成本值?

    一个好的折衷方案是让您的服务器在大约 100 毫秒内创建散列的成本值。

    这是一个简单的测试来找到这个值:

    1. /* 100 ms. */
    2. $time = 0.1;
    3. /* Initial cost. */
    4. $cost = 10;
    5. /* Loop until the time required is more than 100ms. */
    6. do
    7. {
    8. /* Increase the cost. */
    9. $cost++;
    10. /* Check how much time we need to create the hash. */
    11. $start = microtime(true);
    12. password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
    13. $end = microtime(true);
    14. }
    15. while (($end - $start) < $time);
    16. echo 'Cost found: ' . $cost;

    找到成本后,每次执行password_hash()时都可以使用它,就像前面的示例一样。

     

    使用password_needs_rehash()使您的哈希保持最新

    为了理解这一步,让我们看看 password_hash()是如何 工作的。

    password_hash()接受三个参数:

    1. 您需要散列的密码
    2. 您要使用的哈希算法
    3. 传递给散列算法的选项数组

    PHP 支持不同的散列算法,但您通常希望使用默认的一种。

    您可以使用PASSWORD_DEFAULT常量来选择默认算法,正如您在前面的示例中看到的那样。

     

    截至 2020 年 6 月,默认算法为 Bcrypt。

    但是,如果实现了更好、更安全的算法,PHP 将来可以更改默认算法。

    发生这种情况时,  PASSWORD_DEFAULT 常量将指向新算法。因此,所有新的哈希都将使用新算法创建。

    但是,如果您想使用以前的算法生成的所有旧哈希,并使用新的自动重新创建它们怎么办?

     

    这就是password_needs_rehash()发挥作用的地方。

    此函数检查是否已使用给定的算法和参数创建了哈希。

    例如:

    1. /* Password. */
    2. $password = 'my secret password';
    3. /* Set the "cost" parameter to 10. */
    4. $options = ['cost' => 10];
    5. /* Create the hash. */
    6. $hash = password_hash($password, PASSWORD_DEFAULT, $options);
    7. /* Now, change the cost. */
    8. $options['cost'] = 12;
    9. /* Check if the hash needs to be created again. */
    10. if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
    11. {
    12. echo 'You need to rehash the password.';
    13. }

    如果当前默认散列算法与用于创建散列的算法不同,则 password_needs_rehash()返回true

    password_needs_rehash() 还检查选项参数 是否不同。

    如果您想在更改 Bcrypt 成本等参数后更新哈希值,这将非常方便。

     

    此示例显示了如何在远程用户登录时自动检查密码哈希并在需要时对其进行更新:

    1. /* Include the database connection script. */
    2. include 'pdo.php';
    3. /* Set the "cost" parameter to 12. */
    4. $options = ['cost' => 12];
    5. /* Login status: false = not authenticated, true = authenticated. */
    6. $login = FALSE;
    7. /* Username from the login form. */
    8. $username = $_POST['username'];
    9. /* Password from the login form. */
    10. $password = $_POST['password'];
    11. /* Remember to validate $username and $password. */
    12. /* Look for the username in the database. */
    13. $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
    14. /* Values array for PDO. */
    15. $values = [':name' => $username];
    16. /* Execute the query */
    17. try
    18. {
    19. $res = $pdo->prepare($query);
    20. $res->execute($values);
    21. }
    22. catch (PDOException $e)
    23. {
    24. /* Query error. */
    25. echo 'Query error.';
    26. die();
    27. }
    28. $row = $res->fetch(PDO::FETCH_ASSOC);
    29. /* If there is a result, check if the password matches using password_verify(). */
    30. if (is_array($row))
    31. {
    32. if (password_verify($password, $row['account_passwd']))
    33. {
    34. /* The password is correct. */
    35. $login = TRUE;
    36. /* Check if the hash needs to be created again. */
    37. if (password_needs_rehash($row['account_passwd'], PASSWORD_DEFAULT, $options))
    38. {
    39. $hash = password_hash($password, PASSWORD_DEFAULT, $options);
    40. /* Update the password hash on the database. */
    41. $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
    42. $values = [':passwd' => $hash, ':id' => $row['account_id']];
    43. try
    44. {
    45. $res = $pdo->prepare($query);
    46. $res->execute($values);
    47. }
    48. catch (PDOException $e)
    49. {
    50. /* Query error. */
    51. echo 'Query error.';
    52. die();
    53. }
    54. }
    55. }
    56. }

    如何自动转换旧哈希

    在此示例中,您将实现一个简单的脚本来自动将旧的、基于 MD5 的哈希转换为使用password_hash()创建的安全哈希。

    这是它的工作原理:

    • 当用户登录时,您首先使用password_verify()检查其密码。
    • 如果登录失败,请检查数据库中的哈希是否是密码的MD5哈希。
    • 如果是,则使用password_hash()生成的哈希值更新哈希值。

     

    这是脚本:

    1. /* Include the database connection script. */
    2. include 'pdo.php';
    3. /* Set the "cost" parameter to 12. */
    4. $options = ['cost' => 12];
    5. /* Login status: false = not authenticated, true = authenticated. */
    6. $login = FALSE;
    7. /* Username from the login form. */
    8. $username = $_POST['username'];
    9. /* Password from the login form. */
    10. $password = $_POST['password'];
    11. /* Remember to validate $username and $password. */
    12. /* Look for the username in the database. */
    13. $query = 'SELECT * FROM accounts WHERE (account_name = :name)';
    14. /* Values array for PDO. */
    15. $values = [':name' => $username];
    16. /* Execute the query */
    17. try
    18. {
    19. $res = $pdo->prepare($query);
    20. $res->execute($values);
    21. }
    22. catch (PDOException $e)
    23. {
    24. /* Query error. */
    25. echo 'Query error.';
    26. die();
    27. }
    28. $row = $res->fetch(PDO::FETCH_ASSOC);
    29. /* If there is a result, check if the password matches using password_verify(). */
    30. if (is_array($row))
    31. {
    32. if (password_verify($password, $row['account_passwd']))
    33. {
    34. /* The password is correct. */
    35. $login = TRUE;
    36. /* You can also use password_needs_rehash() here, as shown in the previous example. */
    37. }
    38. else
    39. {
    40. /* Check if the database contains the MD5 hash of the password. */
    41. if (md5($password) == $row['account_passwd'])
    42. {
    43. /* The password is correct. */
    44. $login = TRUE;
    45. /* Update the database with a new, secure hash. */
    46. $hash = password_hash($password, PASSWORD_DEFAULT, $options);
    47. $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
    48. $values = [':passwd' => $hash, ':id' => $row['account_id']];
    49. try
    50. {
    51. $res = $pdo->prepare($query);
    52. $res->execute($values);
    53. }
    54. catch (PDOException $e)
    55. {
    56. /* Query error. */
    57. echo 'Query error.';
    58. die();
    59. }
    60. }
    61. }
    62. }

    结论

    在本教程中,您学习了如何使用password_hash()password_verify()来创建密码的安全散列(以及为什么不应该使用 MD5)。

    您还学习了如何通过设置适当的Bcrypt 成本并根据需要自动重新散列密码来使密码散列更加安全。

     

    现在轮到你了:在下面留下你的问题和评论。

  • 相关阅读:
    R语言 利用tmap绘制分级色彩地图
    vue3 利用 Composition provide 实现祖先组件向后代组件传值
    3.4 齐次方程组基础解系
    k8s 部署RocketMQ主从
    Spring Cloud Gateway针对指定接口做响应超时时间限制
    Django系列16-员工管理系统实战--echar图表统计
    天鹰340亿(AquilaChat2-34B-16K)本地部署的解决方案
    【C++】模拟实现STL容器:vector
    【JAVA】继承和多态的子类和父类代码执行顺序研究
    [附源码]计算机毕业设计JAVAjsp研究生管理系统
  • 原文地址:https://blog.csdn.net/allway2/article/details/126682114