サイトの投稿欄、登録欄などに、form を利用した際、ユーザが入力内容を送信完了した後も、ブラウザにその内容が残ってしまい、もう一度送信をクリックして、2重ポストになってしまうことがあります。
更にこれが悪用されて、DoS 攻撃されてしまう、なんて恐れもあるわけで、対策必要ですね。
前回、PHP Mailer を利用したフォームの作成についてまとめたので、これを引き続き使って、メール送信フォームの場合を考えてみたいと思います。
やりたいこと
3段階でメールを送信するように作ります。
「form.php」 入力画面。「確認」ボタンで「check.php」へ。
↓ ↑
「check.php」 確認画面。「送信」ボタンで「submit.php」へ。「戻る」ボタンで「form.php」へ。
↓
「submit.php」 完了画面。送信の成功、失敗を伝える。
このような流れなのですが、満たしたい条件は以下の通りです。
- 「form.php」で入力した内容は、送信完了まで維持。まだ送信完了していない状態なら「check.php」から「form.php」に戻っても、入力画面に初めに入力した内容が表示される。
- 一度送信したら、入力内容はブラウザから消去させる。2重送信させない。
→ 解決方法 セッションを利用
→ 解決方法 ワンタイムチケットを生成
1.は、PHP のセッション機能を利用すればいいわけですが、2.のワンタイムチケットの考え方について、次にまとめます。
ワンタイムチケット
1番目の「form.php」で、ランダムな文字列の $ticket を生成。
それを$_SESSION[‘ticket’] = $ticket でセッション関数に格納。
フォームの中で、<input type=”hidden”> で、$ticket を POST メソッドで送信。
という流れになるのですが、言葉より図の方が分かりやすい気がするので、図を作ってみました。
これを見ていただければ思うのですが、「submit.php」で送信終了後、ブラウザの「戻る」で「check.php」を表示した場合など、
$_POST[‘ticket’] 、 $_SESSION[‘ticket’] が存在しない、またはこの二つの値が一致しない場合、通常のこちらが期待する流れでないことが分かります。そのような状況で、初期化するなどするような仕組みに作ればよいことになります。
2重ポストできないフォーム作成
ページ例 → https://exp.hazu.jp/mail-form/form.php
(submit.php がないので、「送信」をクリックしてもメールは送れませんが、check.php までは見れます。)
form.php (入力画面)
<?php
// セッション開始
session_start();
// キャッシュしない
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Cache-Control: post-check=0, pre-check=0', false );
header( 'Pragma: no-cache' );
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
//文字コードをUTF-8にする
mb_internal_encoding('UTF-8');
header("Content-Type: text/html; charset=UTF-8");
// ワンタイムチケットを生成
$ticket = md5(uniqid(rand(), TRUE));
// ワンタイムチケットをセッション変数に格納
$_SESSION['ticket'] = $ticket;
// HTMLでのエスケープ処理をする関数
function h($string) {
return htmlspecialchars($string, ENT_QUOTES);
}
?>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>メール送信フォーム</title>
</head>
<body>
<h1>メール送信フォーム</h1>
<form action="check.php" method="post">
<div>氏名: <input type="text" name="name" id="name" value="<?php echo h(@$_SESSION['name']);?>"></div>
<div>メールアドレス: <input type="text" name="to" id="to" value="<?php echo h(@$_SESSION['to']);?>"></div>
<div>件名: <input type="text" name="subject" id="subject" value="<?php echo h(@$_SESSION['subject']);?>"></div>
<div>本文: <br />
<textarea name="message" id="message" cols="50" rows="10"><?php echo h(@$_SESSION['message']);?></textarea>
</div>
<input type="hidden" name="ticket" value="<?php echo h($ticket);?>" />
<div><input type="button" onclick="submit();" value="入力内容を確認する"></div>
</form>
</body>
</html>
ちょっと説明足します。
16行目: ワンタイムチケットを生成しています。
$ticket = md5(uniqid(rand(), TRUE));
rand 関数は、ランダムな数桁の整数を生成します。uniqid 関数は、ランダムな13文字の文字列を生成します。この二つが合体して、同じチケットが生成される可能性はほぼゼロとなります。
さらに、md5 関数で、MD5ハッシュ値を計算して、変数に格納します。
19行目: ワンタイムチケットをセッション変数に格納しています。
このセッション関数は、submit.php でセッションが破棄されるまで、維持されることになります。
35~39行目: フォームの入力欄には、初期の状況では空、check.php から「戻る」ボタンで戻って表示された場合には、最初に入力した内容が表示されるようにします。input 要素ではvalue 属性に、textarea 要素では<textarea> と </textarea> の間に、セッション関数を表示させるようにしますが、初期値が空になるように、関数の前に @ を付けて Null を返せるようにしておきます。
41行目: 画面には表示されない状態で、$ticket を、次の check.php へ POST メソッドで受け渡します。
check.php (確認画面)
<?php
// セッションの開始
session_start();
// キャッシュしない
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Cache-Control: post-check=0, pre-check=0', false );
header( 'Pragma: no-cache' );
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
//文字コードをUTF-8にする
mb_internal_encoding('UTF-8');
header("Content-Type: text/html; charset=UTF-8");
// チケットを確認
if (isset($_POST['ticket']) && isset($_SESSION['ticket'])) {
$ticket = $_POST['ticket'];
if ($ticket != $_SESSION['ticket']) {
header('Location: form.php');
exit();
}
} else {
header('Location: form.php');
exit();
}
// HTMLでのエスケープ処理をする関数
function h($string) {
return htmlspecialchars($string, ENT_QUOTES);
}
// 入力内容の取得・変数に格納
$name = $_POST['name'];
$to = $_POST['to'];
$subject = $_POST['subject'];
$message = $_POST['message'];
// 入力値をセッション変数に格納
$_SESSION["name"] = $name;
$_SESSION["to"] = $to;
$_SESSION["subject"] = $subject;
$_SESSION["message"] = $message;
?>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>メール送信フォーム</title>
</head>
<body>
<h1>入力内容確認</h1>
<form action="submit.php" method="post">
<div>氏名: <?php echo h($name); ?></div>
<div>メールアドレス: <?php echo h($to); ?></div>
<div>件名: <?php echo h($subject); ?></div>
<div>本文: <br />
<?php echo h($message); ?></textarea>
</div>
<input type="hidden" name="ticket" value="<?php echo h($ticket);?>" />
<div>
<input type="button" onClick="history.back()" value="戻る" />
<input type="button" onclick="submit();" value="送信" />
</div>
</form>
</body>
</html>
16~25行目: ここで、POST メソッドで送られてきた ticket と、セッション変数に格納されている ticket を確認します。
$_POST[‘ticket’]、$_SESSION[‘ticket’] 、どちらか一方でもない場合 → 強制的に form.php に転送
$_POST[‘ticket’] と $_SESSION[‘ticket’] の値が一致しない場合 → 強制的に form.php に転送
22~42行目: 入力値は $_POST で取得し、それを更に、$_SESSION でセッション変数に格納し、submit.php にも、form.php に戻った場合にも持ち運べるようにします。
60行目: form.php と同じですが、画面には表示されない状態で、$ticket を、次の form.php へ POST メソッドで受け渡します。
submit.php (完了画面)
メール送信には PHP Mailer を使う前提で書いています。→ PHP Mailer を利用したフォームの作成
<?php
// セッションの開始
session_start();
// キャッシュしない
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Cache-Control: post-check=0, pre-check=0', false );
header( 'Pragma: no-cache' );
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
//文字コードをUTF-8にする
mb_internal_encoding('UTF-8');
header("Content-Type: text/html; charset=UTF-8");
// メール日本語対応
mb_language("japanese");
// チケットを確認
if (isset($_POST['ticket']) && isset($_SESSION['ticket'])) {
$ticket = $_POST['ticket'];
if ($ticket !== $_SESSION['ticket']) {
echo '<html>有効期限切れ <a href="form.php" title="ホーム">入力画面へ戻る</a></html>';
exit();
}
} else {
echo '<html>有効期限切れ <a href="form.php" title="ホーム">入力画面へ戻る</a></html>';
exit();
}
// HTMLでのエスケープ処理をする関数
function h($string) {
return htmlspecialchars($string, ENT_QUOTES);
}
// 変数にセッション変数を代入
$name = $_SESSION['name'];
$to = $_SESSION['to'];
$subject = $_SESSION['subject'];
$message = $_SESSION['message'];
// PHPMailer クラスをネーム空間にインポート
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
// Composer の autoloader をロード
require 'vendor/autoload.php';
// インスタンス生成
$mail = new PHPMailer(true);
try {
//Server settings
$mail->isSMTP(); // SMTP 利用
$mail->Host = 'smtp.gmail.com'; // SMTP サーバー(Gmail の場合これ)
$mail->SMTPAuth = true; // SMTP認証を有効にする
$mail->Username = 'nana.rome@gmail.com'; // ユーザ名 (Gmail ならメールアドレス)
$mail->Password = 'gvrvkkcewyspzffl'; // パスワード
$mail->SMTPSecure = 'tls'; // 暗号化通信 (Gmail では使えます)
$mail->Port = 587; // TCP ポート (TLS の場合 587)
// メール本体
$mail->setFrom('nana.rome@gmail.com', 'hazuki'); // 送信元メールアドレスと名前
$mail->addAddress($to, mb_encode_mimeheader($name, 'ISO-2022-JP')); // 送信先メールアドレスと名前
$mail->Subject = mb_encode_mimeheader($subject, 'ISO-2022-JP'); // 件名
$mail->Body = mb_convert_encoding($message, "UTF-8","AUTO"); // 本文
// 送信
$mail->send();
echo '送信済み';
// セッション変数を破棄
$_SESSION = array();
session_destroy();
} catch (Exception $e) {
echo "送信失敗: {$mail->ErrorInfo}";
}
?>
19~28行目: check.php と同じですが、ここで、POST メソッドで送られてきた ticket と、セッション変数に格納されている ticket を確認します。
$_POST[‘ticket’]、$_SESSION[‘ticket’] 、どちらか一方でもない場合 → form.php へ誘導
$_POST[‘ticket’] と $_SESSION[‘ticket’] の値が一致しない場合 → form.php へ誘導
73, 74行目: メール送信が完了したら、セッションを破棄します。