パスワードリセット時の有効期限つきワンタイムURL

今回解説していくのはパスワードリセット時の有効期限付きワンタイムURLの作り方です
流れは

- パスワードリセットのページでメールアドレスを入力
- メールアドレスが登録されていても登録されていなくても同じ文言を出力
- メールアドレスが登録されていた場合は有効期限付きワンタイムURLが記載されたメールを送信する
- パスワードを更新した場合は``5秒後``にログイン画面にリダイレクト

```sendMail.php
<?php
if (isset($_POST['submit']) && isset($_POST['email'])) {
	$email = $_POST['email'];
	if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
		try {
			$dbh = new PDO('mysql:host=localhost; dbname=dbname;', 'username', 'password');
		} catch (PDOException $e) {
			exit('接続失敗:' . $e->getMesssage());
		}
		$exists_email_sql = 'SELECT mail FROM members WHERE mail = :mail';
		$exists_email_stmt = $dbh->prepare($exists_email_sql);
		$exists_email_stmt->execute([':mail' => $email]);
		$exists_email = $exists_email_stmt->fetch();
		$msg = $email . "宛にパスワード再発行URLを送信しました。\n30分間のみ有効です";
		if ($exists_email) {
			$url = 'https://example.com/reset.php?key=';
			$secret_key = md5(uniqid(mt_rand(), true));
			$url .= $secret_key;
			mb_language('Japanese');
			mb_internal_encoding('UTF-8');
			$title = 'パスワード再発行';
			$content = "パスワード再発行URLは以下の通りです。30分間のみ有効です\n " . $url;
			$headers = 'From: example@from.com';
			//なぜか送れなかったので'-f'以降(エラー用送信先)を付け加えました。$headerまでで基本はいけると思います。
			mb_send_mail($email, $title, $content, $headers, '-f' . 'example@forError.com'); 
			$reset_date = date('Y-m-d H:i:s');
			$update_sql = 'UPDATE members SET reset_date = :reset_date, secret_key = :secret_key WHERE mail = :mail';
			$update_stmt = $dbh->prepare($update_sql);
			$params = [
				':reset_date' => $reset_date,
				':secret_key' => $secret_key,
				':mail' => $email,
			];
			$update_stmt->execute($params);
		}
	} else {
		$msg = 'メールアドレスを入力してください';
	}
}
?>
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>パスワードリセット</title>
</head>
<body>
<?php
if (isset($_POST['submit']) && isset($email)) :
	echo $msg;
else :
?>
<form method='post'>
<div>登録したメールアドレスを入力してください</div>
<input type='email' name='email' placeholder='example@gmail.com' required>
<input type='submit' name='submit' value='送信'>
</form>
<?php endif; ?>
</body>
</html>
```
まずはメールアドレスが入力されていて、送信ボタンが押されていたら
入力フォームを出さないようにします
メールアドレスのバリデーションは``filter_var()``関数を使うのが無難
正規表現には諸説あるので。
メールアドレスの形式が正しければ、次にメールアドレスがDBに登録されているか調べる
``boolean``の返り値を返す変数名には以下を参考にしてください

```boolean変数名
is+形容詞
has+過去分詞
can+動詞の原型
exists+名詞
has+名詞
should+動詞の原型
```

今回は``has_email``か``exists_email``でいいと思う
あとは``registered_email``とかかな

**セキュリティ上メールが登録されていようがなかろうが同じ文言を返すようにするため**
先に文言を代入しておく

**メールアドレスが登録されていた場合のポイントは2つ**
まず``ワンタイムURL``にすること
それから``時間制限を設ける``こと

ここで乱数をurlに追加しているのはメール受信者のみアクセス可能にするためです。
次のページでは有効な乱数がurlについていない場合はパスワードを入力した次の画面でエラー文言が出るようにしています。
**また、乱数の頭や、urlに時刻を含めるのも容易に改竄ができてしまうためNGです**

ここでは後に乱数が有効か調べるために、乱数をDBに保存しています。
また、後に時間制限の確認を行うために、メール送信時の時刻もDBに保存しています。

メールの送信方法は以下の二つが必要です

- 文字エンコードの指定
- 言語の指定

マルチバイトの文字を扱う場合は
``mb_send_mail(送信先、タイトル、本文、省略可能追加ヘッダ、省略可能追加コマンドパラメータ)``
を使いましょう

```
mb_send_mail(string $to, string $subject, string $message[, mixed $additional_headers = NULL[, string $additional_parameter = NULL]] ): bool
```
今回使用した`-f`以降はエラー用のメール送信先なので、送信先のメールアドレスと同じものを指定すれば良いと思います

```reset.php
<?php
$msg = '不正なアクセスです';
$redirect_login_flg = false;
if (isset($_POST['change']) && isset($_POST['pass']) && isset($_GET['key'])) {
	$pass = $_POST['pass'];
	$key = $_GET['key'];
	if (!empty($pass)) {
		try {
			$dbh = new PDO('mysql:host=localhost; dbname=dbname;', 'username', 'password');
		} catch (PDOException $e) {
			exit('接続失敗:' . $e->getMesssage());
		}
		$is_valid_sql = 'UPDATE members SET pass = :pass, secret_key = NULL WHERE :reset_date <= date_add(reset_time, INTERVAL 30 MINUTE) AND secret_key = :secret_key';
		$is_vallid_stmt = $dbh->prepare($is_valid_sql);
		$params = [
			':reset_date' => date('Y-m-d H:i:s'),
			':pass' => $pass,
			':secret_key' => $key,
		];
		$is_valid_stmt->execute($params);
		$is_valid = $is_valid_stmt->rowCount();
		if ($is_valid) {
			$redirect_login_flg = true;
			$msg = "パスワードの更新が完了しました。\nこのページは5秒後にログインページへリダイレクトします";
		}
	} else {
		$msg = "パスワードを入力してください\n<a href=\"board_reset.php?key=" . $key . ">戻る</a>\n";
	}
}
?>
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<?php if ($redirect_login_flg) : ?>
<meta http-equiv="refresh" content=" 5; url=./home.php">
<?php endif; ?>
<title>パスワード再発行</title>
</head>
<body>
<?php
if (isset($_POST['change']) && isset($_POST['pass'])) :
	echo $msg;
else :
?>
<form method='post'>
<div>新しいパスワードを入力してください</div>
<input type='password' name='pass' placeholder='password' required>
<input type='submit' name='change' value='変更'>
</form>
<?php endif; ?>
<a href='board.php'>ホームに戻る</a>
</body>
</html>
```
ここでも同様にformの出しわけを行っています
パスワードが正しく入力された場合は、シークレットキーを基に、該当するレコードを探して、``secret_key``を``NULL``に変更し
``password``をユーザーが入力したものに変更します。
``secret_key``を``NULL``にすることで、次回以降URLが使えなくなるので**ワンタイムURL**にすることができます
なお``reset_date``は変更しなくても良いでしょう。
変更しなければ、パスワードを更新した日にちがわかりますね。

``$redirect_login_flg``を設定することでパスワードを正常に変更できたあとは5秒後にリダイレクトされるようになっています。
これは

```
<meta http-equiv="refresh" content=" 秒数; url=相対パスか絶対パス">
```
で実装することができます。
他の方法もあるので調べてみてください

#補足
``html``側の``input``で``required``などの入力を制限していてもバックエンド側での制御は必要です。
``html``側の制限は入力をアシストするためのものと考えてください
実際に``Chrome DevTools``を使って、制限を消してフォームを送信することができるので