PHP – password_hashで生成したパスがpassword_verifyで一致しないとき

password_hash()で作られたパスワードハッシュ値をpassword_verify()で一致させる方法について解説したいと思います。まずは、基本的な使い方を説明して最後にフォームを使ってDBのデータと一致しているかを解説していきたいと思います。

password_verify()の例

<?php
$password = 'password';

$hash = password_hash($password, PASSWORD_DEFAULT);

if(password_verify($password, $hash)){
	echo "OK.";
}else{
	echo "NG.";
}
?>

このコードはもっとも簡単な例ですが、password_hash()で生成したハッシュとパスワードを検証した例です。上記のコードを実行するとOK.と表示されるでしょう。

password_verify()はパスワードとハッシュ値が一致した場合trueを返し、それ以外はfalseを返します。

一致しない失敗例 – ダブルクオーテーションで囲っている

<?php
// password_hash()を使って"password"をハッシュ化したもの
$hash = "$2y$10$xHJp9syKZnC0vg/lFlUosOBDXhO0BRNUr1hxPqusnrKuFhvRI249i";

if(password_verify('password', $hash)){
	echo 'OK.';
}else{
	echo 'NG.';
}
?>

password_hash()で作られたパスワードハッシュ値がpassword_verify()でマッチしない場合ダブルクォーテーションで囲っている場合が多い。それをシングルクオーテーションで囲いましょう。

なぜ、ダブルクオーテーションだとダメなのかは説明するまでもないですが、PHPではダブルクオーテーションは変数を展開してしまうからです。上記のコードを実行すればNotice: Undefined variable: xHJp9syKZnC0vg....とエラーが表示されます。なので下記のように修正します。

// NG
$hash = "$2y$10$xHJp9syKZnC0vg/lFlUosOBDXhO0BRNUr1hxPqusnrKuFhvRI249i";

// OK シングルクオーテーションで囲む
$hash = '$2y$10$xHJp9syKZnC0vg/lFlUosOBDXhO0BRNUr1hxPqusnrKuFhvRI249i';

一致しない失敗例 – 前後に空白がある

基本的にはpassword_hash()で生成したハッシュの文字数は$2y$crypt フォーマットで文字数は常に60文字になります。当然前後に空白があればハッシュが変わってしまうので空白は削除するようにしましょう。

// password_hash()を使って"password"をハッシュ化したもの
$hash = ' $2y$10$xHJp9syKZnC0vg/lFlUosOBDXhO0BRNUr1hxPqusnrKuFhvRI249i ';

$hash = trim($hash);

if(password_verify('password', $hash)){
	echo 'OK.';
}else{
	echo 'NG.';
}

一致しない失敗例 – パスワードハッシュ値でデータベースと照合しようとしている

password_hash()で生成されたパスワードは常にランダムな形で生成します(文字数は固定ですが)。なのでパスワードハッシュの値とデータベースに入っているハッシュのパスワードを照合しようとしても一致しないでしょう。

<?php 
$password = 'password';
echo password_hash($password, PASSWORD_DEFAULT);
?>

// 1回目
$2y$10$xaUZabYHzGQQ1aUPBcPF9er7cEH14BR.TOA/NHzRVt4CJrD0/i7TK

// 2回目
$2y$10$3d3wdgv7FWfPtO1eGGn5L.13CTiMeA7cnz69/oTjFsd1rf.GII7k6

// 3回目
$2y$10$wNOfqgYLL1dbZ2oufjKOM.IjnKDgSRLSXcfFkvjpPvU0kz8wvumea

下記のコードはPOSTで受け取ったパスワードをハッシュ値にしてテーブルに格納されているハッシュパスワードとパスワードを探そうとしている間違い。

if($_SERVER["REQUEST_METHOD"] == 'POST'){
	$pass = password_hash($_POST['password'], PASSWORD_DEFAULT);
	$stmt = $pdo->prepare('SELECT * FROM account where password = ?');
	$stmt->bindValue(1, $pass, PDO::PARAM_STR);
	$stmt->execute();
}

では、データベースにあるハッシュ化されたパスワードと入力したパスワードを一致させるにはどうしたらいいのか。

この場合はwhere文で別のカラムで一致条件を探してレコードを抜き出してからパスワードを取得し、取得したパスワードをpassword_verify()で一致するか調べます。


<?php 

$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$password = 'root';

try{
	$pdo = new PDO($dsn, $user, $password);
	$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}catch(PDOException $e){
	die("エラー:" . $e->getMessage());
}


if($_SERVER["REQUEST_METHOD"] == 'POST'){
	$email = $_POST['email'];
	$pass = $_POST['password'];
	
	$stmt = $pdo->prepare('SELECT * FROM account where email = ?');
	$stmt->bindValue(1, $email, PDO::PARAM_STR);
	$stmt->execute();
	$res = $stmt->fetch(PDO::FETCH_ASSOC);

	if($res && password_verify($pass, $res['password'])){
		echo "一致";
	}else{
		echo "一致しない";
	}

}
?>
<form action="" method="post">
	<input type="text" name="email">
	<input type="text" name="password">
	<input type="submit" value="ログイン">
</form>

上記のコードを捕捉すると入力されたメールアドレスを探すSQL文となっています。メールアドレスがあれば$resに結果が入り、$res = $stmt->fetch(PDO::FETCH_ASSOC)で結果セットを連想配列で取得しています。

あとは、入力したパスワードとデータベース内にあるパスワードハッシュが一致しているかどうかをpassword_verify()で検証しパスワードが一致しているかしていないかの処理になります。

参考文献