フォームの二重送信を防止する方法

 Web開発において、同一のフォームが重複して送信されると困る場合がよくあります。

 今回は、以下の3つの観点において、フォームの二重送信を防止する方法を紹介します。

 

ボタン連打による二重送信

 フォームの送信ボタンを連打、もしくはキーボードのEnterキーを連打することによる二重送信を防ぐために、JavaScriptを使用します。

 送信ボタンが押された時に、そのボタン自体を無効化することで、再度送信ができないようにします。

 ※この方法はJavaScriptが無効な環境では利用できません。

 以下に例を示します。

html

<button id="submit-button" type="submit">送信</button>

JavaScript

window.onload = function() {
  document.getElementById('submit-button').addEventListener('click', function() {
    this.disabled = true;
  });
}

画面のリロードによる二重送信

 ブラウザの更新ボタン等、画面のリロードによるフォームの二重送信を防ぐためには、POST/Redirect/GETPRG)パターンを使用します。

 PRGパターンは設計パターンの一つで、フォーム送信後のブラウザの再読み込みなどをスムーズに行うためのものです。

 

1.POST:まず、ユーザーがフォームにデータを入力し、そのデータをサーバーにPOSTリクエストとして送信します。このデータはサーバー上で処理され、データベースに保存されるなどします。

2.Redirect:サーバーは、データの処理が終わった後に、ユーザーを別のURLにリダイレクトします。これは通常、POSTリクエストが正常に完了したことを示すページ(「送信完了ページ」など)です。

3.GET:ユーザーのブラウザは、リダイレクト先のURLGETリクエストとしてサーバーに問い合わせ、その結果をユーザーに表示します。

 

 PRGパターンを使用することで、ユーザーが画面をリロードした場合でも、最後のHTTPリクエスト、つまりリダイレクト(GETリクエスト)が再度行われるだけで、フォームデータの送信(POSTリクエスト)は再度行われません。

 以下はPRGパターンのイメージとサーバーサイドのコードの例です。(※例ではLaravelを使用しています)

class Controller
{
    public function post(Request $request)
    {
        // データベース保存処理など
        // ...

        // 送信完了画面を表示
        return view('submit-end');
    }
}

 

class Controller
{
    public function post(Request $request)
    {
        // データベース保存処理など
        // ...

        // リダイレクト
        return redirect('/submit-end');
    }

    public function end(Request $request)
    {
        // 送信完了画面を表示
        return view('submit-end');
    }
}

 

ブラウザバックによる二重送信

 ブラウザの「戻る」ボタン等によって、フォームの入力画面に戻り、その後再度送信ボタンを押されることによる二重送信を防ぐために、トークンを使用します。

 サーバーサイドで一意のトークンを生成しセッションに保存します。
 そして、そのトークンをフォームに埋め込み、フォームと一緒にクライアントに送ります。フォームが送信された時、サーバーは送信されてきたトークンとセッションに保存されたトークンが一致するかどうかを確認します。一致しない場合や、トークンが存在しない場合は、二重送信、または不正な送信とみなして処理を行わないようにします。

 以下に例を示します。(※例ではLaravelを使用しています)

 

 コントローラー

class Controller
{
    public function get(Request $request)
    {
        // 一意のトークンを生成
        $token = create_token();

        // トークンをセッションに保存
        session(['token' => $token]);

        // トークンをビューに渡す
        return view('form', ['token' => $token]);
    }

    public function post(Request $request)
    {
        // セッションからトークンを取得
        $token = session('token');

        // トークンが一致しない場合
        if ($token !== $request->token)
        {
            // 400 Bad Request を返す
            abort(400);
        }

        // トークンを削除
        session()->forget('token');

        // トークンが一致した場合の処理
        // ...
    }
}

 

 ビュー

<form method="POST" action="{{route('post')}}">
  @csrf
  <input type="hidden" name="token" value="{{$token}}">
  <input type="text" name="name" value="">
  <input type="text" name="email" value="">
  {{-- ... --}}
  <button id="submit-button" type="submit">送信</button>
</form>

まとめ

・連打による二重送信→JSでボタンを無効化する

・リロードによる二重送信→PRGパターンを使用する

・ブラウザバックによる二重送信→トークンを使う

 今回紹介した方法以外にも、フォーム送信後、一定時間フォームの再送信をロックする、フォーム送信時から、サーバーが応答を返すまでの間、ローディング画面を表示したりするなど、他にもフォームの二重送信を防止する手法はあります。

 ただし、これらの手法はすべて、ユーザー体験やそれぞれの対策がどのようなケースで有効なのかを把握し、それぞれを組み合わせて対策を行うことが重要です。

 

お問い合わせはこちら