質問: $request->only('name', 'email', 'message'); //DBに保存 Contact::create($data); //メール送信 Mail::to($to)->send(new ContactMail($data)); //この行がなければ多重送信防げる // 多重送信対策 $request->session()->regenerateToken(); return redirect()->action('ContactController@thanks'); } ``` ルーティング ``` Route::get('/contact', 'ContactController@form'); //入力画面表示 Route::post('/contact', 'ContactController@send'); //送信処理 Route::get('/thanks', 'ContactController@thanks'); //完了画面表示 ``` formメソッドとthanksメソッドは画面を表示するだけです。 ``` public function form() { return view('form'); } public function thanks() { return view('thanks'); } ``` メール送信処理があると、なぜ多重送信されてしまうのでしょうか。 また、完了画面に直接リンクできる状態ですが、不自然でしょうか。 この場合、sendメソッドから完了画面にリダイレクトするときにセッションを付与して、thanksメソッドで確認する等の処理が最適ですか。 回答よろしくお願いします。
回答: `regenerateToken()`の効果が出るのが間に合っていないからではないでしょうか。 セッションがあるリクエストの処理は大雑把に言うと以下のようになります。 * リクエストの処理をはじめるときにクライアントから送られてきたセッションIDをもとにセッション情報をよみだす * リクエストの処理の本体。この例でいうと`send`。 * 最後にセッション情報を書き出します。 この一連の処理をロックして行う流儀とロックはしない流儀があります。ロックする方だとこのような問題は起きないのですがLaravelのセッションはロックしない方です。(その分ロックする方は同じセッションのリクエストは一度に一つしか処理できないので効率が悪いという弱点があるので一長一短ではあります) メール送信はそこそこ時間のかかる処理なので、送信連打があると一つ目の`regenerateToken()`で更新された新しいトークンを持ったセッション情報を書き出す前に別のリクエストが動きはじめることがあって、そのときトークンが古いままで処理されるのでCSRFトークン不一致になることなく処理が継続できてしまいます。 メール送信を入れる前は起きなかったとありますが、おそらく同じく問題になるタイミングは短いながらもあるのですが、人間が連打したぐらいではなかなか起きないということではないかと思います。 対策ですがブラウザ側でjavascriptなどで対策する方法とサーバ側で対策する方法が考えられます。 サーバ側でやる場合は、使用済みトークンかどうか判定するのがいいのではないでしょうか。csrfトークンが使用済みであることを記録し、入り口でチェックします。 例: `Cache::add`はキーが登録済みなら`false`を返すのでこういう用途には便利です。`regenerateToken`で新たに作られたトークンを含んだセッション情報が保存されるまで持てばいいので保持期間は1分もあれば十分でしょう。 ``` if (!Cache::add('used_token.'.$request->session()->token(), 1, 1)) { # 使用済みだったときの処理 # 例: TokenMismatchExceptionを投げる(CSRFトークン不一致の扱いにする) # 独自のエラー表示にする など } ``` またメール送信は遅いので別プロセスで後で処理するようにするというのもよく行われます。Laravelだとキューの仕組みがありますから、リクエストの処理ではキューに積むだけで送信は後でやるというようなことも比較的簡単に実現できます。