Rails アプリケーションで、iPhone の Safari からのリクエストでだけ ActionController::InvalidAuthenticityToken
例外が発生するので調査していたところ、Mobile Safari は再起動後、開いていたタブをロードする際に、POST リクエストで得たページであっても再度リクエストを送信しているらしいことがわかった。
※ iOS 13.5.1 の iPhone 11 で検証
検証方法
下記の Ruby スクリプトを作成して ruby server.rb -o 0.0.0.0
で起動
# gem install sinatra が必要
require "sinatra"
html = <<~HTML
<!doctype html>
<form action="/" method="post">
<input type="text" name="email">
<button type="submit">Submit</button>
</form>
HTML
get "/" do
html
end
post "/" do
p params
html
end
- Mobile Safari で
http://[上記スクリプト実行したPCのアドレス]:4567
にアクセス - 適当に入力して submit
- Mobile Safari を終了
- Mobile Safari を起動 (確認などなしでページが表示される)
この手順でログを見ると、4 で 2 と同じリクエストが送られていることが確認できた。
x.x.x.x - - [08/Jul/2020:14:24:37 +0900] "GET / HTTP/1.1" 200 131 0.0072
{"email"=>"foo"}
x.x.x.x - - [08/Jul/2020:14:24:41 +0900] "POST / HTTP/1.1" 200 131 0.0022
{"email"=>"foo"}
x.x.x.x - - [08/Jul/2020:14:24:48 +0900] "POST / HTTP/1.1" 200 131 0.0007
Rails で InvalidAuthenticityToken 例外が起こる理由と対策
Rails で...
- Cookie をセッションストアとして使っていて、かつ有効期限が Session (デフォルト)
-
protect_from_forgery with: :exception
を指定している
場合に、上記のように 2 度 POST リクエストが送られると、2 度めのリクエストでは Cookie がクリアされているので session[:_csrf_token]
が nil になり、params[:authenticity_token]
の検証が失敗し、InvalidAuthenticityToken 例外が発生してしまう。
解決策としては下記のものが考えられる。
- POST でページを render せず、redirect してかならず GET させるようにする
- protect_from_forgery で null_session などにする
- Cookie の有効期限を伸ばす
3 の場合は config/initializers 下に適当なファイルを作って下記の内容を記述すればよい。
# key の部分はなんでもいいがデフォルトは "_#{アプリケーション名}_session"
# ここでは有効期限を 2 週間にしている
Rails.application.config.session_store :cookie_store, key: "_foo_session", expire_after: 2.weeks