ブラウザには「更新ボタン」「F5ボタン」で画面を更新する機能があります。
これはこれで大事な機能なのですが、フォームの送信ボタンを押した後に、更新すると、今ほど送信した内容が、未入力でも再送信されます。
これはブラウザの機能であり、ブラウザが同じ内容を再送信するため、受信するPHP側ではどうにもできません。
しかしながら、重複送信であることをPHP側で認識することができれば、そのブラウザからの起動をスルーして、二重処理を防ぐことができます。
テクニックで重複送信を回避できましたので、忘備録として記録いたします。
現象はこのような感じです。
百聞は一見です。
こちらのサンプル1をご覧ください。
フォームに何か文字を入力し、送信すると画面に同じ内容が表示されます。
そのままブラウザの「更新」または「F5ボタン」を押すと、フォームに未入力状態でも、同じ内容が送信されます。
原因はブラウザの直前処理保持機能
ブラウザには直前の処理を保持する機能があります。
更新ボタン・F5ボタンを押したときに、直前の処理を再現するためです。
プラグラム側から見れば、なんともはた迷惑な機能ですが、ブラウザの機能である以上、送信元側での対処はできません。
なので受信側で対処するしかありません。
ということで、ググって調べて対処しました。
対処したブログラムがこちら
まずはこちらのサンプル2をご覧ください。
先ほどのサンプル1とは異なり、フォーム送信後更新ボタン・F5ボタンを押しても直前の送信処理が行われません。
仕組みは、ブラウザ側からは再送信されていますが、プログラム側で再送信処理を判断して、再送信ならば処理をスルーしています。
疑問?どうやって二重投稿を判断しているの?
いったいどうやって二重投稿を判断しているのか?
と思われているかと思います。
今回は「トークン判定方式」で二重投稿をスルーしています。
トークン判定ってなに?
と思われことでしょう。
簡単に言うと、ブラウザ側とプログラム側に照合番号を与え、送信時に一致している場合のみ処理を実行します。
処理実行後、新しい照合番号を発番します。
ブラウザに表示する毎に照合番号が変化しますが、ブラウザ更新、F5ボタンを押した時は、前回と同じ照合番号がプログラム側に送信されます。
この特性を利用して判断しています。
疑問?仕組みはわかったけど、具体的にはどうやっているの?
サンプル2では具体的にどうやって、二重投稿スルーを実現しているのか?
ということで、サンプル2のソースを以下に記述いたします。
<?php
// (2)▼▼▼ -----------------------
// セッションスタート
session_start();
// (2)▲▲▲ ここまで --------------
// 初期メッセージセット
$msg = "文字を入力して送信ボタンを押してください";
// (4)▼▼▼ -----------------------
if ((isset($_REQUEST["name"]) == true) // フォームボタンが押された?
&& (isset($_REQUEST["send"]) == true)) // 送信ボタンが押された?
{
// (4)▲▲▲ ここまで --------------
// (5)▼▼▼ -----------------------
if ((isset($_REQUEST["chkno"]) == true) && (isset($_SESSION["chkno"]) == true)
&& ($_REQUEST["chkno"] == $_SESSION["chkno"])) // トークン番号が一致?
{
// 入力文字を表示
$msg = "今入力された値は<br>【".$_REQUEST["name"]."】です。";
}
else
{
// 更新・F5ボタンによる再投稿をガード
$msg .= "<br>更新・F5を押しても、再投稿はされません";
}
// (5)▲▲▲ ここまで --------------
}
// 日付情報をセット
$today = date("Y/m/d h:i:s");
$time = "<br>■ただ今の時間[${today}]";
// (3)▼▼▼ -----------------------
// 新しいトークンをセット
$_SESSION["chkno"] = $chkno = mt_rand();
// (3)▲▲▲ ここまで --------------
?>
<!doctype html>
<html>
<head>
<meta charset="shift_jis">
<title>再投稿テスト</title>
<style type="text/css">
article
{
width: 800px;
margin-right: auto;
margin-left: auto;
border: 1px solid #CCC;
text-align: center;
padding: 30px;
border-radius: 6px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
}
p
{
text-align: justify;
}
</style>
</head>
<body>
<article>
<section>
<h1><?php echo $msg ?></h1>
</section>
<section>
<form action="" method="post" enctype="multipart/form-data">
<!-- (1)▼▼▼ ----------------------->
<input name="chkno" type="hidden" value="<?php echo $chkno; ?>">
<!-- (1)▲▲▲ ここまで -------------->
<input name="name" type="text">
<input name="send" type="submit" value="送信">
<input name="reset" type="submit" value="リセット">
</form>
</section>
<section>
<p>送信後ブラウザの更新ボタンまたはF5ボタンを押しても再投稿されません。<?php echo $time; ?></p>
</section>
</article>
</body>
</html>
(1)まずはHTMLのフォーム部分から解説いたします。
プログラムは全体を見てもさっぱりなものです。
こういった場合は、パーツごとに見ていくとわかりやすいです。
まずは、送信フォーム部分に仕掛けを入れています。
<form action="" method="post" enctype="multipart/form-data">
<!-- (1)▼▼▼ ここに照合番号を挿入----------------------->
<input name="chkno" type="hidden" value="<?php echo $chkno; ?>">
<!-- (1)▲▲▲ ここまで -------------->
<input name="name" type="text">
<input name="send" type="submit" value="送信">
<input name="reset" type="submit" value="リセット">
</form>
上記①の部分が照合番号部分です。
ここに非表示データで毎回変わる照合番号を表示します。
これでブラウザからの送信時に、照合番号が送られるようになります。
(2)セッション処理を起動します。
// (2)▼▼▼ -----------------------
// セッションスタート
session_start();
// (2)▲▲▲ ここまで --------------
ブラウザからの照合番号とプログラム側の照合番号を比較するためには、プログラム側が照合番号を覚えている必要があります。
しかしPHPは一回実行したらすべてを忘れてしまう悲しい特性があります。
そこで、セッションを利用して照合番号を保存します。
セッションとは、PHP実行した後でも、セットした情報を覚えている特性があります。
※セッション処理は、セッションハイジャックなど虚弱性の要因の一つになります。使う場合にはセッション対策を講じて使用しましょう。(ちなみにこのプログラムはサンプルのためセッション対策は入っていません)
(3)新しい照合番号を発番する
// (3)▼▼▼ -----------------------
// 新しいトークンをセット
$_SESSION["chkno"] = $chkno = mt_rand();
// (3)▲▲▲ ここまで --------------
照合番号を発番し、ブラウザ側と、セッションに保存します。
毎回違う番号を発番するために、mt_rand()関数(ランダム数字発行)を利用しています。
(4)フォームからの送信の確認
// (4)▼▼▼ -----------------------
if ((isset($_REQUEST["name"]) == true) // フォームボタンが押された?
&& (isset($_REQUEST["send"]) == true)) // 送信ボタンが押された?
{
// (4)▲▲▲ ここまで --------------
このソースは表示と受信が一体になっているため、「表示処理」なのか「受信処理」なのかを切り分ける必要があります。
PHPではパラメータを受信すると、$_REQUEST変数に値が入ります。
入力テキストボックスの’name’値が入っているかどうかで判定しています。
その下の(isset($_REQUEST[“send”]) == true)は送信ボタンが押されたのか、リセットボタンが押されたのかを判断しています。
リセットが押されたときは何もせず初期表示を行います。
(5)二重投稿の判定
// (5)▼▼▼ -----------------------
if ((isset($_REQUEST["chkno"]) == true) && (isset($_SESSION["chkno"]) == true)
&& ($_REQUEST["chkno"] == $_SESSION["chkno"])) // トークン番号が一致?
{
// 入力文字を表示
$msg = "今入力された値は<br>【".$_REQUEST["name"]."】です。";
}
else
{
// 更新・F5ボタンによる再投稿をガード
$msg .= "<br>更新・F5を押しても、再投稿はされません";
}
// (5)▲▲▲ ここまで --------------
$_REQUEST変数と$_SESSION変数に”chkno”(照合番号)が保存されているかを確認します。
その上で$_REQUEST[“chkno”](ブラウザ側の照合番号)と$_SESSION[“chkno”](プログラム側の照合番号)が一致しているかを判定します。
照合番号が一致していれば正規送信として処理をします。
照合番号が不一致ならば、二重投稿として処理をスルーしています。
トークン方式はブラウザのオウム返しを逆手に取った手法
なぜ照合番号を毎回変えるだけで二重投稿が判断できるのかというと、ブラウザの更新ボタンまたはF5ボタンを押したときに送信される照合番号は1回前の照合番号だからです。
ブラウザで更新ボタンが押された時には、プログラム側の照合番号は新しい番号に変わっているため、必ず照合番号は不一致になります。
無論、すべての二重投稿現象に対応できるわけではありません。
例えば送信ボタンの連打や、ヒストリバック後の再送信などです。
ただ、単純なブラウザの再更新処理による二重送信は、今回の方式でガードできます。
またご参考になれば幸いです。