【Formの二重送信防止方法】PHPページでF5を押すとフォームが二重送信されてしまう
PHPでプログラムしていると、FormのあるページでSubmit後、F5を押すとFormのデータが二重送信されてしまう問題が発生します。
今回はこの二重送信問題の解決方法をご紹介します。
PHPのフォームデータ二重送信を防ぐ方法は大まかに2種類あるので、それぞれ個別に説明します。
Formの二重送信を防ぐ方法1:トークン方式
このトークン方式は、ページを開くたびにトークン(一時的な暗号)を生成し、トークンが一致するか確認して、一致する時だけ処理を行うという方法です。
2回目にPOSTが発生した時には既に新しいトークンが発行されており、トークンが一致しなくなるので、再読み込みは行われるものの、処理は実行されないということになります。
とりあえず、サンプルコードを出します。
<?php
// セッションを開始
session_start();
// トークンが一致するか調べる
if ($_REQUEST["POST_TOKEN"] === $_SESSION["POST_TOKEN"]) {
// トークンが一致するので、何か処理する
~~~~
}
// 新しいトークンを生成する
$_SESSION["POST_TOKEN"] = uniqid();
// この時点でトークンが更新されるため、再読み込みを行ってもトークンが一致しなくなる
// トークンが一致しなくなるので、FormからSubmitを行わず、2回目にPOSTされたとしても処理は実行されない
?>
<form action="" method="POST">
<input type="text" name="name" placeholder="名前を入力してください">
<input type="submit" name="add_name" value="送信する"/>
<!-- 画面には出ないが、送信データにトークンを仕込んでおく -->
<input type="hidden" name="POST_TOKEN" value="<?php echo $_SESSION["POST_TOKEN"]; ?>"/>
</form>
これは不正アクセスのチェックではないので、トークンの生成方式はセキュアな方法でなくても構いません。
// トークンが一致するか調べる
if ($_REQUEST["POST_TOKEN"] === $_SESSION["POST_TOKEN"]) {
// トークンが一致するので、何か処理する
~~~~
}
ここで、トークンが一致するか調べています。
$_REQUEST[“POST_TOKEN”] | formタグ内の input[type=hidden] の value が入ります。 $_REQUESTには$_GETと$_POSTの両方の値が入ります。恐らく・・・ |
$_SESSION[“POST_TOKEN”] | $_SESSION[“POST_TOKEN”] = uniqid(); で代入した値が入っています。 |
結果的に同じ値が入るのですが、$_REQUEST[“POST_TOKEN”] の方はformがsubmitされた時にしか更新されないというのがミソです。
同じページに再度アクセスされた時、submitされたことによるアクセスなのか、F5でのアクセスなのかをここで判別しているということになります。
なぜかトークン方式で、トークンが一致しない場合
URLの正規化などで.htaccessを設定している場合、勝手にリダイレクトがかかって、予期せぬトークンの上書きが起きている可能性があります。
もし、どれだけテストしてもトークンが一致しない場合、セッションを使って、ページビューのカウントを取ってみてください。
<?php
session_start();
// セッション"session_cnt"が空なら1を代入、空以外なら+1して、表示する
echo $_SESSION["session_cnt"] = isset($_SESSION["session_cnt"])? $_SESSION["session_cnt"] + 1: 1;
?>
値が2ずつ進んでいたら、どこかで予期せぬリダイレクトが発生しています。
まず、.htaccessを見直してみることです。
例えば、HTTPからHTTPSに転送する設定や、”wwwなし”から”wwwあり”に転送する設定が.htaccessに記述されていると、知らない間に同じページに自動リダイレクトが発生していることがあります。
また、canonicalが悪さしている可能性もあります。
Formの二重送信を防ぐ方法:リダイレクト方式
もう一つの方法は同じページに自動でGETリダイレクトをかける方法です。
具体的には、フォームデータをPOST送信し、受け取り側はPOSTで送信された時のみ処理し、処理の最後にGETでリダイレクトする方法です。
<?php
// POSTで呼ばれた
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// フォームが送信された時の処理を行う
~~~~
// 現在のページにGETリダイレクトをかける
header("Location: " . $_SERVER['REQUEST_URI']);
exit;
}
?>
<form action="" method="POST">
<input type="submit" value="送信する">
</form>
処理の流れを加筆しました。
このようにすることで、もう一度F5を押したときには、GETで送信されるため、POSTの処理が繰り返されることがなくなります。
ただし、この方法にはいくつかデメリットがあります。
- formがPOSTしか使えなくなる
もし、この方法で二重送信防止制御を行っていて、GETリクエストの処理を作ってしまうと、そこは二重送信されてしまいます。 - サーバーの負荷が増えるかも
一度ページを読み込んで、またリダイレクトをかけているので、サーバーの負荷や稼働時間が伸びてしまうんじゃないかと思います。
クラウドサーバーとか使ってる場合、稼働時間が増えると、コストに直結しますよね。
また、リダイレクトすること自体が効率的じゃないんじゃないかとか思うので、私はこの方法は使っていないです。(検証してないので、本当はこっちの方が効率的な可能性もあります。)
そもそも、なぜ同じページにPOSTしているの?
これはセキュリティの問題があるのですが、例えばform内に以下のような記述があるとしましょう。
<form action="/functions/item/add.php" method="POST">
<label>商品名</label>
<input type="text" name="itemName" placeholder="商品名を入力してください。"/>
<input type="submit" value="商品を追加する"/>
</form>
どう思いますか?
「/functions/item/delete.phpがありそうだな。itemIdっていう値で/functions/item/delete.phpにPOSTしたら、商品を削除できるんじゃないか?」
「/functions/user/add.phpがありそうだな。userNameとpasswordっていう値でPOSTしたらユーザー追加できちゃったりして」
私ならこう考えます。
actionの中身は空(action=””)が標準です。
ここに何かパスを入力すると、ハッカーにサーバー内のディレクトリ構造を教えてあげているようなものです。
actionの中身が空の場合、submit後、formがある同じURLに戻ってきます。
同じURLに戻ってくるので、F5を押すと、POSTされた値を保持したまま、もう一度POSTが送信されるので、二重送信されてしまうというロジックとなります。
<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') { // POSTで呼ばれた場合
if ($_POST['add_item']) { // 商品追加ボタンがsubmitされた場合
require '/functions/item/add.php'; // 商品追加処理を行うPHPを呼び出す
}
}
?>
<form action="" method="POST">
<label>商品名</label>
<input type="text" name="itemName" placeholder="商品名を入力してください。"/>
<input type="submit" name="add_item" value="商品を追加する"/>
</form>
同じページにPOSTして、POSTされた内容によって処理を分ける場合は上記のようになります。
さらに二重送信を防止する場合、以下のようになります。
<?php
session_start(); // セッションを開始
// トークンが登録されているか判定
if (isset($_REQUEST["POST_TOKEN"]) && isset($_SESSION["POST_TOKEN"])) {
// トークン番号が一致するか判定
if ($_REQUEST["POST_TOKEN"] === $_SESSION["POST_TOKEN"]) {
if ($_SERVER['REQUEST_METHOD'] == 'POST') { // POSTで呼ばれた場合
if ($_POST['add_item']) { // 商品追加ボタンがsubmitされた場合
require '/functions/item/add.php'; // 商品追加処理を行うPHPを呼び出す
}
}
}
}
// 上のif文で二重送信のチェックは完了しているため、新しいトークンをセット
$_SESSION["POST_TOKEN"] = uniqid();
?>
<form action="" method="POST">
<label>商品名</label>
<input type="text" name="itemName" placeholder="商品名を入力してください。"/>
<input type="submit" name="add_item" value="商品を追加する"/>
<!-- 画面には出ないが、送信データにトークンを仕込んでおく -->
<input type="hidden" name="POST_TOKEN" value="<?php echo $_SESSION["POST_TOKEN"]; ?>" />
</form>
これで二重送信を防止することが出来ます。
もしお困りごとがありましたら、お問い合わせフォームよりご相談ください。