こんにちは!oda@エンジニア1年目です!
以前の記事で、今後の課題としていたprotect_from_forgery
について調べたので、今日はその内容について書きます。
調べる中で、コントローラーにprotect_from_forgery
を追加すればActionController::InvalidAuthenticityToken
を解消できるという情報をいくつか見ました。
試しに、コントローラーにprotect_from_forgery
を追加してみると、エラーは出ず、問題が解決されたように見えました。
ただ、これだけではなぜエラーが出なくなったのかわからないので、もう少し詳しく調べてみます。
CSRFとは?
まずは発生していたエラーをあらためて確認します。
Can't verify CSRF token authenticity. Completed 422 Unprocessable Entity in 3ms (ActiveRecord: 0.0ms | Allocations: 468) ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
CSRFトークンの信頼性が確認できないと言われていますが、そもそもCSRFとは何でしょうか。
CSRF(クロスサイトリクエストフォージェリ)とは、あるサイトにログインしている状態で、メールや別サイトに悪意を持って仕掛けられたリンクをクリックすると、ログイン中のサイトにリクエストが送られ、本人の意思とは違う操作をさせられるというような問題のことでした。
例をあげると、以下のようになります。
- ユーザーが掲示板サイトAにログインする
- サイトAにログインしたまま、悪意を持って仕掛けられた別サイトのリンクBをクリックする
- リンクBには、サイトAに不適切な書き込みをするリクエストが埋め込まれている
- サイトAはログイン中のユーザーからのリクエストなので、適切なリクエストとして処理する
- ユーザーの意思とは関係なく、サイトAに不適切な書き込みがされる
CSRF対策として、Railsではビューの<head>
タグ内にある<meta name="csrf-token" content="XXXXXXXXXXXX">
のcontent
が、リクエストに含まれているかチェックし、含まれていなければ上記のエラーが発生します。
この<meta name="csrf-token" content="XXXXXXXXXXXX">
はそのサイト自身しか知らないため、別サイトからの不正なリクエストが防げるという仕組みです。
protect_from_forgeryについて
protect_from_forgery
は上記のCSRF対策として使われています。
protect_from_forgery with: :exception
をコントローラーに追加することで有効にすることができますが、デフォルトで有効になっているので特に記載する必要はありません。(参考:Railsガイド)
こちらが有効になっているため、前回の記事で試したようにauthenticity_token
が含まれていないリクエストをするとエラーが発生してしまいました。
さて、冒頭で述べていた
コントローラー に
protect_from_forgery
を追加すればActionController::InvalidAuthenticityToken
を解消できるという情報をいくつか見ました
というのは何だったのでしょうか?
オプションを渡さずにprotect_from_forgery
を追加するということは、protect_from_forgery with: :null_session
を追加することと同義のようです。(参考:Ruby on Rails API)
protect_from_forgery with: :null_session
はauthenticity_token
の検証がされていないリクエストを許可するという設定なので、私が見た情報はCSRF対策をしないことでエラーを回避するというものだったようです。(参考:Ruby on Rails API)
この方法が必要な場面もあるかもしれませんが、私の目的はCSRF対策をしないことではないので、今回についてはこの解決方法は適切ではありません。
X-CSRF-Tokenヘッダにトークンを設定する
以前の記事を書いた後、社内で「X-CSRF-Token
ヘッダにトークンを設定する」という方法を教えていただきました。
次のとおりコードを修正して試してみると、こちらの方法でも同じように投稿内容を削除することができました。
function deletePost(post_id) { const token = $('meta[name="csrf-token"]').attr('content'); $.ajax({ url: `/posts/${post_id}`, // data: { authenticity_token: token }, 削除 headers: {'X-CSRF-Token' : token }, // 追加 type: 'DELETE', dataType: 'json', }) .done((data) => { console.log('削除しました'); }) .fail((data) => { console.log('削除に失敗しました'); }) };
あらためて、Railsガイドを読み直してみると、
Ajax呼び出しに他のライブラリを使う場合は、そのライブラリのAjax呼び出しのデフォルトのヘッダーにセキュリティトークンを追加する必要があります。
という記述がありましたので、この方法がオーソドックスな方法のようです。(参考:Railsガイド)
最初に調べたときはしっかりと読めていませんでした。反省。
今回は、該当箇所にheaders
をベタ書きしていますが、ajaxSetup
メソッドを使うとデフォルトで設定することができそうです。
やればやるほど、試したいことが芋づる式に増えていきます。
ふたたびbutton_toメソッドに戻ってくる
さらにあれこれ調べていると、もともと使っていたbutton_to
メソッドには:remote
というAjaxで処理ができるオプションがあることがわかりました。
さっそく、次のようにオプションを指定して試してみると、自分で書いたAjaxの処理と同じことが、いとも簡単に実現できました。
<%= button_to "削除する", post_path(@post), method: :delete, remote: true %>
さいごに
最終的には、button_to
メソッドにremote: true
を指定するだけでやりたいことが実現できたという、あっけない結論となりました。
今回使った:remote
オプションについては、自分が使っていなかっただけで使われているコードは見たことがあったので、きちんと理解できていれば、あっという間に解決していたはずです。
ただ、寄り道をしたからこそ、CSRF対策についてや、そのためにRailsが裏側で何をしてくれているかということを調べるきっかけになりました。
Railsが裏側でうまく処理してくれていて、私が意識できていないことはまだまだあるはずです。
今後も手を動かす中で知識を増やし、エンジニアとしての基礎を固めて行きたいと思います!