PHP初心者向けのTODOアプリの作り方⑤パスワードのハッシュ化と検索機能

1071 回閲覧されました
みなさんこんにちは、jonioです。
今回はパスワードのハッシュ化と検索機能を実装します。
目次 [隠す]
ログインパスワードのハッシュ化とは
ログインする時のパスワードですがデータベースにそのまま保存されている状態です。
↓はusersテーブルです。
今の私のスキルでは他人のデータベースに接続する方法を知りません。
でもスキルがある人だと何らかのやり方でデータベースに接続できてそれが実現するとパスワードが丸見えの状態になります。
これはセキュリティ上良くないですよね。
だからパスワードを別の文字列に変換するのですがこれをハッシュ化といいます。
文字列への変換はランダムに行われるのではなく一定の規則に基づいて変換されるので同じパスワードを何度ハッシュ化しても同じ文字列になります。
ちなみに私が「taro」をハッシュ化すると「$2y$10$CED/5u4LW4B3wKS92yUE..HldJsdBk0ZJHeHQ5LRckH.S3ft27U3」になります。(ハッシュ化した値はパソコンによって異なります)
ハッシュ化後のパスワードからハッシュ化前のパスワードを辿ることもできます。
図にすると下記になります。
ハッシュ化してログインする流れ
今回実装するのに新しい内容は下記になります。
- パスワードのハッシュ化をpassword_hash( )で行う
- ハッシュ化される前と後の値が等しいかをpassword_verify( )で確認する
これをふまえた上でハッシュ化したパスワードでログインする流れはこうなります。
- 新規登録する時にパスワードをハッシュ化する(最初に設定した名前(田中太郎)とパスワード(taro)はハッシュ化されません)
- フォームに入力したパスワードと「password_hash( )」でハッシュ化したパスワードが「password_verify( )」を使って一致するかを確認する
- フォームに入力したパスワードとハッシュ化したパスワードが同じならログインする
今の状態では名前とパスワードは「名前:田中太郎・パスワード:taro」しかなくてパスワードのハッシュ化ができないので名前とパスワードを登録するページ(signUp.php)を作りまずは名前とパスワードを登録するページを作ります。
名前とパスワードを追加するページ(signUp.php)の作成
新たにsignUp.phpを作ります。
signUp.phpのコードを下記にします。
<?php | |
require("db_connect.php"); | |
$name = $_POST["name"]; | |
$password = $_POST["password"]; | |
$submit = $_POST["submit"]; | |
if(!empty($submit)){ | |
$pdo = db_connect(); | |
try{ | |
$sql = "insert into users (name,password) values(:name,:password)"; | |
$stmt = $pdo -> prepare($sql); | |
$stmt -> bindParam(":name",$name); | |
$password_hash = password_hash($password,PASSWORD_DEFAULT); | |
$stmt -> bindParam(":password",$password_hash); | |
$stmt -> execute(); | |
echo '<p class="signup">登録処理が完了しました</p>'; | |
}catch(PDOException $e){ | |
echo $e -> getMessage(); | |
} | |
} | |
?> | |
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>ユーザ新規登録ページ</title> | |
<link rel="stylesheet" href="css/style.css"> | |
</head> | |
<body> | |
<div class="title-area"> | |
<h1>ユーザ新規登録ページ</h1> | |
</div> | |
<form action="" method="POSt"> | |
<input type="text" class="input-area" name="name" placeholder="Your Name" required><br> | |
<input type="password" class="input-area" name="password" placeholder="Your Password" required><br> | |
<input type="submit" class="input-area submit" name="submit" value="新規登録"> | |
</form> | |
<a href="login.php" class="new">ログインページに戻る</a> | |
</body> | |
</html> |
cssに下記を追加してください。
.new{ | |
color:#fff; | |
text-decoration:none; | |
display: block; | |
margin-top: 20px; | |
} |
これでsignUp.phpにアクセスすると下記の表示になります。
名前を「田中太郎2」・パスワードを「taro2」と入力して「新規登録」のボタンを押してください。
すると新規登録ができず「SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column ‘password’ at row 1」のエラーが表示されて新規登録ができません。
ターミナルで「select * from password;」を使ってusersテーブルを見るとパスワードが追加されていないのが確認できます。
エラーになる理由
エラーの意味は「passwordカラムに登録する値は文字が長すぎます」という意味です。
ターミナルでpasswordカラムに登録できる文字数を「desc users;」と入力して確認します。
すると下記になりますがvarchar(8)となっているので8文字までしかパスワードには使えないのが確認できます。
使える文字数を長くすればいいので80文字にします。
ターミナルで「ALTER TABLE users MODIFY password varchar(80);」と入力します。
そして新規登録のページ(signUp.php)で名前を「田中太郎2」・パスワードを「taro2」と入力して登録すると下記の画像の画面になり登録ができます。
ハッシュ化のやり方と確認
データベースのusersテーブルを見ると名前とパスワードが追加されてハッシュ化されているのが確認できます。
signUp.phpの13行目ですがテーブルのカラムに値を追加するのでINSERT文を使っています。
ハッシュ化は16行目で第二引数に「PASSWORD_DEFAULT」のはこんなもんと思っていいです。
password_hash( )についてはこの記事に詳しい説明があります。
残りのコードは今までと同じなので省略します。
これでハッシュ化は完成したのでログインページ(login.php)でログインします。
ハッシュ化した状態でログインする
パスワードの「taro2」をハッシュ化すると「$2y$10$CED/5u4LW4B3wKS92yUE..HldJsdBk0ZJHeHQ5LRckH.S3ft27U3.」ですがパスワード欄にこれを入力するのは嫌ですよね。
一般的に登録した人がパスワードをハッシュ化されているなんて知らないし普通は「taro2」を使うので「taro2」でログインできるようにしないといけません。
そこで「password_verify()」を使って入力したパスワードをハッシュ化した時に正解のパスワードになるかの確認をすることでログインできる様にします。
「password_verify()」は「password_verify(ハッシュ化する前の値,ハッシュ化した後の値)」と書きますがこの記事が詳しく説明してます。
実装
login.phpのif文を↓にします。
if($row = $stmt -> fetch(PDO::FETCH_ASSOC)){ | |
if(password_verify($password,$row['password'])){ | |
header("Location:main.php"); | |
exit; | |
} | |
}else{ | |
echo '<p color="red">パスワードか名前に間違いがあります。</p>'; | |
} |
これでログインしてもまだ上手くいきません。
理由はlogin.phpの「try{ }」の中の「$sql = “select * from users where name = :name and password = :password”;」 ですが「password = :password」があります。
これはハッシュ化する前のパスワードを使うことになるのですがハッシュ化した後のパスワードを使いたいのでこのままではエラーになります。
それでは意味がないので「password = :password」を取ります。
これで「try{ }」は↓になります。
try { | |
$sql = "SELECT * FROM users WHERE name = :name"; | |
$stmt = $pdo->prepare($sql); | |
$stmt->bindParam(':name', $name); | |
$stmt->execute(); | |
} |
これだとパスワードは違うけど名前は同じ人がいる場合ログインはどうなるんだろう?と思いますがif文でパスワードの認証ができるので人が同じでもパスワードが変われば別の人と認識されるので問題ないです。
これでパスワードのハッシュ化の完成です。
更にセキュリティを上げる場合は↓を参考にして下さい。
最後に簡単な検索機能の追加をします。
簡単な検索機能
簡単な検索機能というのは「同じ語句を含むレコードの内容を表示する」という意味です。
同じ語句でもひらがな・カタカナ・漢字の識別はできません。
だから「でもタイトル」で検索に引っかかっても「でもたいとる」では検索に引っかかりません。
普通はひらがな・カタカナも認識できるので検索の機能としては弱過ぎて商品として使えないですが練習としてはいいと思います。
main.phpには投稿した内容が表示されています。
私の場合だと↓です。
「タイトル」・「本文」の内容で検索して検索した内容のレコードを表示できるようにします。
下記が例ですが検索欄に「でもタイトル1」と入力しています。
実装の考え方
↓の流れで実装します。
- main.phpに検索のフォームを設置
- フォームから送られた情報を変数として受け取る
- 受け取った変数の中身をpostsテーブルのタイトルカラム・本文カラムにあるか探しあった場合は該当するレコードの内容を表示する
- 検索の機能はSQLの「LIKE文」で行う
それでは実装しますがまずはmain.phpにフォームを設置します。
フォームの設置
main.phpのコードを下記にします。(HTMLの部分だけを載せます)
<!doctype html> | |
<html lang="ja"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> | |
<title>メインページ</title> | |
</head> | |
<body> | |
<h1>メインページ</h1> | |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th scope="col">記事ID</th> | |
<th scope="col">タイトル</th> | |
<th scope="col">本文</th> | |
<th scope="col">作成日</th> | |
<th scope="col">編集</th> | |
<th scope="col">削除</th> | |
</tr> | |
</thead> | |
<tbody> | |
<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) : ?> | |
<tr> | |
<td><?php echo $row["id"]; ?></td> | |
<td><?php echo $row["title"]; ?></td> | |
<td><?php echo $row["content"]; ?></td> | |
<td><?php echo $row["time"]; ?></td> | |
<td><a href="edit.php?id=<?php echo $row['id'];?>">編集</a></td> | |
<td><a href="delete.php?id=<?php echo $row['id'];?>">削除</a></td> | |
</tr> | |
<?php endwhile; ?> | |
</tbody> | |
</table> | |
<a href="create.php" ><button type="button" class="btn btn-primary">新規登録</button></a> | |
//ここから追加 | |
<form action="" method="POST"> | |
検索キーワード:<input type="text" name="search_word"><input type="submit" name="search" value="検索"> | |
</form> | |
//ここまで追加 | |
</body> | |
</html> |
次はフォームから送られた情報を変数(main.phpのHTMLのコードの41行目のname属性の値)として受け取ります。
main.phpのコードを下記にします。(PHPの部分だけを載せます)
<?php | |
require('db_connect.php'); | |
//ここから追加 | |
$search = $_POST['search']; | |
$search_word = $_POST['search_word']; | |
//ここまで追加 | |
$pdo = db_connect(); | |
try{ | |
$sql = "select * from posts order by time"; | |
$stmt = $pdo -> prepare($sql); | |
$stmt -> execute(); | |
}catch(PDOException $e){ | |
echo $e -> getMessage(); | |
die(); | |
} | |
?> |
6行目の「search」・7行目の「search_word」が変数です。
受け取った変数をデータベースから探す
main.phpのコードを↓にします。(PHPの部分だけを載せます)
<?php | |
require('db_connect.php'); | |
$search = $_POST['search']; | |
$search_word = $_POST['search_word']; | |
$pdo = db_connect(); | |
try{ | |
$sql = "select * from posts order by time"; | |
$stmt = $pdo -> prepare($sql); | |
$stmt -> execute(); | |
}catch(PDOException $e){ | |
echo $e -> getMessage(); | |
die(); | |
} | |
//ここから追加 | |
if(!empty($search) && !empty($search_word)){ | |
$search_pdo = db_connect(); | |
try{ | |
$search_sql = "select * from posts where content like '%$search_word%' or title like '%$search_word%'"; | |
$stmt = $search_pdo -> prepare($search_sql); | |
$stmt -> execute(); | |
}catch(PDOException $e){ | |
echo $e -> getMessage(); | |
die(); | |
} | |
} | |
//ここまで追加 | |
?> |
検索する時ですが何も語句を入力せずに検索できたら意味がないです。
そこで19行目のif文があります。(login.phpを初めて作った時のログインの実装方法と考え方は同じです)
22行目は本文とタイトルの内容で検索できますが本文だけで検索するなら「or title like ‘%$search_word%’」を取ってタイトルだけで検索するなら「content like ‘%$search_word%’ or」を取ればいいです。
これで実装は完成です。