PHP密码加密教程(password_hash和password_verify)

Published
2022-07-14
浏览次数 :  441

MD5加密是不安全的,md5加密和password_hash结合起来安全程度就很高。

作为PHP开发者,你肯定要了解怎样安全在数据库存储密码。

早个十多年前,PHP开发者喜欢用MD5和 SHA1加密。

/* User's password. */
$password = 'my secret password';

/* MD5 hash to be saved in the database. */
$hash = md5($password);

MD5和SHA加密算法太简单,有很多网站可以破解。
作为替代,PHP早在很久就升级了密码加密函数password_hash.

PASSWORD_HASH()

使用方法:

/* User's password. */
$password = 'my secret password';

/* Secure password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);

password_hash为什么安全,因为他有很强的散列算法,他同时又添加了一个随机因子来防止暴力破解。

使用password_hash后的密码可以直接存储到数据库。

怎么使用password_hash

首先我们通过sql来创建表:

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;

记住设置password列属性为varchar. varchar是长度可变的文本列。原因是password_hash的哈希大小可能会更改。

现在,通过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();
}

然后添加一个新用户名到accounts表里:


/* 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(); }

然后是验证,验证用户名和密码的长度,验证是合适的文本符,验证是否存在这个用户。永远都要验证input的输入值。

怎样更改用户的密码

首先获取密码然后创建哈希值:

/* 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函数。

函数包含两个参数,第一个是你要验证的密码,也就是用户提交的密码,第二个是从数据表里获取到的hash值。如果用户密码正确,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;
  }
}

注意:你不能对比两组hash值来看他们是否匹配,是因为password_hash创建了salted hashessalted hashes包含随机文本,命名为salt,因此,每次哈希值都是不同的,就算源密码是相同的。

输入下面的代码来看看我们所说的情况:

$password = 'my password';

echo password_hash($password, PASSWORD_DEFAULT);
echo '<br>';
echo password_hash($password, PASSWORD_DEFAULT);

password_verify()只对password_hash创建的密码才有用。

怎样更加提高password_hash的安全性

  1. 增加Bcrypt cost.
  2. 自动更新哈希算法

Bcrypt cost

Bcrypt是password_hash当前默认哈希算法。

这种算法添加一个选项(option)叫做cost,默认的cost的值是10.

通过加大cost的值,你可以设置更难的哈希值。cost值越大,创建哈希的时间越长。

自定义cost值的方法


/* Password. */
$password = 'my secret password';

/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];

/* Create the hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);

但是创建什么样的cost值合适?最好是能让你的服务器在100ms之内创建hash值。

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


/* 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_needs_rehash()来保证你的hash值最新(更新密码)

我们首先来看password_hash的逻辑,他包含三个参数,第一个是你要加密的密码,第二个是哈希的算法,第三个是你要传递给加密算法的数组。默认的加密算法是PASSWORD_DEFAULT,2020年之后,这个默认的加密算法是Bcrypt。也就是PASSWORD_DEFAULT === Bcrypt

然而,PHP可以在未来更改加密算法,如果你想要把以前的加密算法生成的密码更新到以后加密算法生成的哈希值,这里就要用到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.';
}

如果当前默认哈希算法和一开始生成密码的算法不同,这个函数就会返回true.

password_needs_rehash() 同样会检测option参数是否不同。

这就非常有用了如果你想更新你的哈希值当你更新参数如bcrypt cost值之后。

下面一个例子很好的解释了你如何能自动检测密码的哈希值然后更新他们,如果需要的话,当远程用户登录之后:

/* 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函数。

下面是逻辑:当用户登录时,首先通过password_verify来验证密码。

当登录失败时,检查数据库里的哈希值是否是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();
      }
    }
  }
}

结论:


Top