LoginSignup
6
8

More than 3 years have passed since last update.

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

Posted at

今回解説していくのはパスワードリセット時の有効期限付きワンタイム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の返り値を返す変数名には以下を参考にしてください

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

今回はhas_emailexists_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_keyNULLに変更し
passwordをユーザーが入力したものに変更します。
secret_keyNULLにすることで、次回以降URLが使えなくなるのでワンタイムURLにすることができます
なおreset_dateは変更しなくても良いでしょう。
変更しなければ、パスワードを更新した日にちがわかりますね。

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

<meta http-equiv="refresh" content=" 秒数; url=相対パスか絶対パス">

で実装することができます。
他の方法もあるので調べてみてください

補足

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

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8