PHP,MYSQLにて、フォームから送信されるデータの「二重投稿防止」についての質問です。


他のブラウザでは問題ないのですが、
safariで送信ボタンを連打しテストしてみますと二重投稿になってしまいました。
このロジックでは何か不足があるのでしょうか?

現在以下のようなロジックになっております。
--------------▼コード ここから--------------
<?php
//セッションスタート
Session::sessionStart();

//トークン発行
if (!isset($_SESSION['token'])) {
$_SESSION['token'] = Str::getRandomString(24);
}

//POSTかつ二重投稿でなければ保存
if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST['token'] == $_SESSION['token']) {
//トークンを破棄
unset($_SESSION['token']);
//DBへデータを保存
hoge::insert($_POST['data']);
}
?>

<?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
<html><body>
<form method="post" action="tes.php">
<input type="hidden" name="token" value="<?php echo $_SESSION['token'] ?>" />
<input type="text" name="data" />
<input type="submit" value="送信" />
</form>
</body></html>
<?php } ?>
--------------▲コード ここまで--------------

「//DBへデータを保存」の直前に「unset($_SESSION['token'])」しているのにも関わらず、
なぜ複数回DBへインサートされてしまうのでしょうか?

回答の条件
  • 1人30回まで
  • 登録:
  • 終了:2008/12/04 13:25:49
※ 有料アンケート・ポイント付き質問機能は2023年2月28日に終了しました。

ベストアンサー

id:tezcello No.2

回答回数460ベストアンサー獲得回数69

ポイント200pt

session 周りはまだ勉強中ですが...


> unset($_SESSION['token']);されるタイミングですが、

> なんと、、全ての処理が終わった後に初めてされているようです。。

> 理想は、以下の「トークンを破棄」の部分で処理されてほしいのですが、、

当該スクリプトがまだ終了していないうちに同じ SID が呼ばれると、セッションデータは以前のままであるという事ですよね。

セッションを終了してしまいますが session_write_close() というのは使えますか?


二重投稿を防ぐ手段として、「 UNIQUE 制約」をつけてDB側で受け付けないようにするのは反則でしょうか?

他にも、

・最新の(直前の)投稿との直接比較

・同一ホストからは、○秒以内は連続書き込みできない

のような設定のものをよく見た気がします。

id:uniuniko

ご回答ありがとう御座います。


> 当該スクリプトがまだ終了していないうちに同じ SID が呼ばれると、セッションデータは以前のまま


なるほど、そのような性質が問題でしたか、、少しスッキリしました。


「session_write_close()」ですが、これは全く知りませんでした。unset($_SESSION['token']);のすぐ後に session_write_close(); してみたところ、、、、見事に問題が解決されました!!!!


あと、MySQL側でのUNIQUE制約ですが、これは全く考えつきませんでした。どうしても無理そうなら使おうかと思っていましたが、session_write_close();でうまく行きましたので、良かったです。

この度は、誠に有り難う御座いました。

とても勉強になりましたし、本当に助かりましたm(__)m

2008/12/04 13:23:56

その他の回答1件)

id:kn1967 No.1

回答回数2915ベストアンサー獲得回数301

ポイント35pt
if (!isset($_SESSION['token'])) {
    $_SESSION['token'] = Str::getRandomString(24);
} elsif ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST['token'] == $_SESSION['token']) {
    unset($_SESSION['token']);
    hoge::insert($_POST['data']);
}

といったような具合に、初回アクセス時には

データベース書き込み判定処理などに行かないようにすればよろしいかと・・・。

さらに細かく

if (!isset($_SESSION['token'])) {
    $_SESSION['token'] = Str::getRandomString(24);
} elsif ($_SERVER['REQUEST_METHOD'] != 'POST') {
//    不正アクセスの疑いがあるかも・・・
} elsif ($_POST['token'] == $_SESSION['token']) {
    unset($_SESSION['token']);
    hoge::insert($_POST['data']);
} else {
//    不正アクセスの疑いがあるかも・・・
}

などとしても良いかも・・・。

id:uniuniko

ご回答ありがとう御座います。

おっしゃる通りの条件分岐でテストしてみましたが、やはり二重投稿されてしますようです。引き続き試行錯誤してみたいと思います。

おかしいですね、、、

2008/12/04 13:19:04
id:tezcello No.2

回答回数460ベストアンサー獲得回数69ここでベストアンサー

ポイント200pt

session 周りはまだ勉強中ですが...


> unset($_SESSION['token']);されるタイミングですが、

> なんと、、全ての処理が終わった後に初めてされているようです。。

> 理想は、以下の「トークンを破棄」の部分で処理されてほしいのですが、、

当該スクリプトがまだ終了していないうちに同じ SID が呼ばれると、セッションデータは以前のままであるという事ですよね。

セッションを終了してしまいますが session_write_close() というのは使えますか?


二重投稿を防ぐ手段として、「 UNIQUE 制約」をつけてDB側で受け付けないようにするのは反則でしょうか?

他にも、

・最新の(直前の)投稿との直接比較

・同一ホストからは、○秒以内は連続書き込みできない

のような設定のものをよく見た気がします。

id:uniuniko

ご回答ありがとう御座います。


> 当該スクリプトがまだ終了していないうちに同じ SID が呼ばれると、セッションデータは以前のまま


なるほど、そのような性質が問題でしたか、、少しスッキリしました。


「session_write_close()」ですが、これは全く知りませんでした。unset($_SESSION['token']);のすぐ後に session_write_close(); してみたところ、、、、見事に問題が解決されました!!!!


あと、MySQL側でのUNIQUE制約ですが、これは全く考えつきませんでした。どうしても無理そうなら使おうかと思っていましたが、session_write_close();でうまく行きましたので、良かったです。

この度は、誠に有り難う御座いました。

とても勉強になりましたし、本当に助かりましたm(__)m

2008/12/04 13:23:56
  • id:uniuniko
    最初の質問でsafariのみ二重投稿されると言いましたが、他のフラウザでも全て二重投稿されるようです。訂正いたします。

    試しに以下のようにsleep(20)を加え、ボタン連打の他に、「sleep中に複数ウインドウから同様のフォームデータをPOSTする」という検証方法でテストしました。


    //POSTかつ二重投稿でなければ保存
    if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST['token'] == $_SESSION['token']) {
    //トークンを破棄
    unset($_SESSION['token']);

    sleep(20);

    //DBへデータを保存
    hoge::insert($_POST['data']);
    }



    予め2つの同じ内容を持つフォームを開いておき、このフォームのhiddenには両方とも同一のtokenがセットされている状態です。一つ目のフォームから一度目のPOSTし、すぐに二つ目のフォームから二度目のPOSTしました。通常ですと二度目のPOSTではDBへインサートされないはずですが、、されてしまいます。

    ここで、今回の最大の疑問点が出てきます。
    一度目のPOSTで、「トークンを破棄」しているのに、何故か二度目のPOSTでも以下の条件分岐内を処理してしまうという点です。

    if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST['token'] == $_SESSION['token']) {
    //DBへ保存
    }

    二度目のPOSTでは、「$_POST['token'] == $_SESSION['token']」は「$_POST['token'] == 空の$_SESSION['token']」であり、この処理は行われないはずです。
  • id:kn1967
    empty($_POST['token'])も考慮する必要ありそう・・・。
  • id:uniuniko
    ご回答ありがとう御座います。

    emptyも条件分岐に入れてみましたが効果がないようです、、
    ------
    if ($_SERVER['REQUEST_METHOD'] == 'POST' && !empty($_POST['token']) && $_POST['token'] == $_SESSION['token']) {
    ------
  • id:uniuniko
    色々テストしてわかったことがあります。

    unset($_SESSION['token']);されるタイミングですが、
    なんと、、全ての処理が終わった後に初めてされているようです。。
    理想は、以下の「トークンを破棄」の部分で処理されてほしいのですが、、

    //POSTかつ二重投稿でなければ保存
    if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST['token'] == $_SESSION['token']) {
    //トークンを破棄
    unset($_SESSION['token']);


    これでは、二重投稿されるはずです。
    ちなみに、セッションはDBで管理しています。

    引き続き調査してみたいと思います。
  • id:kn1967
    内部の動作については掘り下げていきたいところではありますが
    最悪の場合の逃げとして別の値を入れてしまうというのはいかがでしょう。
  • id:uniuniko
    kn1967さん

    ご回答ありがとう御座います。

    「別の値を入れる」とは $_SESSION['token'] へ別の値を入れるということでしょうか。
    トークン破棄が然るタイミングで行われていない現状ですと、いずれにせよ難しいと思います。

    今度は、phpやアパッチの設定を疑って調べてみます。。
  • id:tezcello
    高ポイント&いるか、ありがとうございます。

    上手く行ったようで、ホッとしてます。
    (まだ、session は使った事が無いので、ビギナーズラックってやつでしょうか...)
  • id:uniuniko
    tezcelloさん

    この度は本当に有り難う御座いました。

この質問への反応(ブックマークコメント)

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

回答リクエストを送信したユーザーはいません