本ガイドでは、コントローラに関連するいくつかの高度なトピックについて学習します。 このガイドの内容:
本ガイドでは、Railsアプリケーションのコントローラに関連する高度なトピックをいくつか取り上げます。Action Controllerの概要については、Action Controller の概要ガイドを参照してください。
クロスサイトリクエストフォージェリ(CSRF)は、Webアプリケーションが信頼しているユーザーになりすまして不正な偽造リクエストを送信する形で行われる、悪意のある攻撃の一種です。
この種の攻撃を回避するために開発者が最初に行うべきステップは、アプリケーション内の「破壊的な」操作(作成、更新、破棄)では常にGET
以外のリクエスト(POST
、PUT
、DELETE
など)を使うようにすること、つまりGET
リクエストで破壊的操作が絶対に行われないようにすることです。
ただし、悪意のあるサイトがGET
以外のリクエストを標的サイトに送信する可能性もあるため、Railsではリクエストフォージェリ(リクエストの偽造)からの保護機能がデフォルトでコントローラに組み込まれています。
この保護は、protect_from_forgery
メソッドでトークンを追加することで行われます。このトークンはサーバーのみが認識しており、リクエストのたびに異なるトークンが追加されます。Railsは、受信したトークンをセッション内のトークンで検証します。受信リクエスト内で適切なトークンと一致しない場合、サーバーはアクセスを拒否します。
config.action_controller.default_protect_from_forgery
がtrue
に設定されている場合、CSRFトークンは自動的に追加されます(新規作成Railsアプリケーションのデフォルト設定)。以下のように手動でも設定できます。
class ApplicationController < ActionController::Base protect_from_forgery with: :exception end
ActionController::Base
のすべてのサブクラスはデフォルトで保護されており、検証されていないリクエストではActionController::InvalidAuthenticityToken
エラーが発生します。
form_with
で以下のようにフォームを生成すると、
<%= form_with model: @user do |form| %> <%= form.text_field :username %> <%= form.text_field :password %> <% end %>
生成されるHTMLのhiddenフィールドに、authenticity_token
という名前を持つCSRFトークンが自動的に追加されます。
<form accept-charset="UTF-8" action="/users/1" method="post"> <input type="hidden" value="67250ab105eb5ad10851c00a5621854a23af5489" name="authenticity_token"/> <!-- fields --> </form>
Railsは、フォームヘルパーによって生成されるすべてのform
要素にこのトークンを追加するため、ほとんどの場合、開発者は何もする必要はありません。フォームを手動で作成する場合や、別の理由でトークンを追加する必要がある場合は、以下のようにform_authenticity_token
メソッドでトークンを利用できるようになります。
<!-- app/views/layouts/application.html.erb --> <head> <meta name="csrf-token" content="<%= form_authenticity_token %>"> </head>
form_authenticity_token
メソッドは、有効な認証トークンを生成します。これは、カスタムAjax呼び出しなど、Railsが認証トークンを自動的に追加しない場所で有用です。
CSRF攻撃やCSRF対策について詳しくは、セキュリティガイドを参照してください。
Rails 8.0からは、ApplicationController
でallow_browser
メソッドを使うことで、デフォルトでは「モダンな」ブラウザの利用のみをユーザーに許可し、古いブラウザではアクセスできなくなります。
class ApplicationController < ActionController::Base # 以下を指定すると、webp画像、web push、バッジ、importmap、CSSネスト、CSS :hasをサポートするモダンブラウザのみが許可される。 allow_browser versions: :modern end
:modern
を指定した場合に許可されるブラウザには、Safari 17.2以上、Chrome 120以上、Firefox 121以上、Opera 106以上が含まれます。使いたい機能がどのバージョンのブラウザでサポートされているかを確認するには、caniuse.comを利用できます。
デフォルトの:modern
以外に、許可したいブラウザバージョンを以下のように手動で指定することも可能です。
class ApplicationController < ActionController::Base # ChromeとOperaについては全バージョンを許可するが、"internet explorer"(ie)はどのバージョンも許可しない。 # Safariは16.4以上、Firefoxは121以上を許可する。 allow_browser versions: { safari: 16.4, firefox: 121, ie: false } end
versions:
オプションにハッシュを渡した場合、ハッシュに一致するブラウザが指定のバージョンより古い場合はブロックされます。つまり、versions:
に明示的に記載していない他のすべてのブラウザ(上の例ではChromeとOpera)や、User-Agent
ヘッダーを通知しないエージェントは、アクセスが「許可される」点にご注意ください。
また、以下のように特定のコントローラでallow_browser
メソッドを書いて、only
オプションやexcept
オプションで特定のアクションのみを許可または拒否することも可能です。
class MessagesController < ApplicationController # ApplicationControllerでブロックされるブラウザの他に、 # showアクションについてはOpera 104未満、Chrome 119未満もブロックする。 allow_browser versions: { opera: 104, chrome: 119 }, only: :show end
ブロックされたブラウザには、デフォルトでHTTPステータスコード406 Not Acceptableでpublic/406-unsupported-browser.html
のエラー表示用ファイルが配信されます。
Railsには3種類のHTTP認証機構が組み込まれています。
HTTP BASIC認証(基本認証)は、ユーザーがWebサイト(または管理者セクションなど、Webサイトの特定のセクション)にアクセスするためにユーザー名とパスワードの入力を要求するシンプルな認証方法です。これらの認証情報(credential)をブラウザのHTTP BASIC認証用ダイアログウィンドウに入力すると、ユーザーの認証情報はエンコードされて、以後のリクエストのたびにHTTPヘッダー経由で送信されます。
HTTP BASIC認証は認証スキームの一種であり、主要なブラウザおよびHTTPクライアントでサポートされています。RailsコントローラでHTTP BASIC認証を使うには、http_basic_authenticate_with
メソッドを利用します。
class AdminsController < ApplicationController http_basic_authenticate_with name: "Arthur", password: "42424242" end
上のコードを実行すると、AdminsController
から継承したコントローラを作成できます。これらのコントローラのすべてのアクションは、HTTP BASIC認証によるユーザー認証情報の入力が必須となります。
HTTP BASIC認証は手軽に実装できますが、ネットワーク経由で送信されるcredentialは暗号化されないため、BASIC認証自体は安全ではありません。BASIC認証を使う場合は、必ずHTTPSプロトコルも併用してください。HTTPSプロトコルを強制する設定も利用できます。
HTTPダイジェスト認証は、暗号化されていないパスワードをクライアントからネットワーク経由で送信する必要がないため、BASIC認証より安全性が高まります。ダイジェスト認証では、認証情報をハッシュ化しを1つ(ユーザー名)だけを受け取たダイジェストを送信します。
Railsでダイジェスト認証を利用するには、authenticate_or_request_with_http_digest
メソッドを使います。
class AdminsController < ApplicationController USERS = { "admin" => "helloworld" } before_action :authenticate private def authenticate authenticate_or_request_with_http_digest do |username| USERS[username] end end end
上の例で示したように、authenticate_or_request_with_http_digest
のブロックでは引数を1つ(ユーザー名)だけ受け取ります。ブロックは、パスワードが見つかった場合はパスワードを返します。nil
またはfalse
が返される場合は、認証が失敗したとみなされます。
トークン認証は「ベアラー(Bearer)認証」とも呼ばれ、クライアントがログインに成功した後に一意のBearerトークンを受け取り、それを以後のリクエストでAuthorization
ヘッダーに含める認証方法です。
クライアントは、リクエストのたびに認証情報を送信する代わりに、このトークン(ユーザーのセッションを表す文字列)を認証の「ベアラー」として送信します。
このアプローチでは、進行中のセッションから資格情報を分離することでセキュリティが向上します。事前に発行された認証トークンを使用して認証を実行します。
Railsでトークン認証を実装するには、authenticate_or_request_with_http_token
メソッドを使います。
class PostsController < ApplicationController TOKEN = "secret" before_action :authenticate private def authenticate authenticate_or_request_with_http_token do |token, options| ActiveSupport::SecurityUtils.secure_compare(token, TOKEN) end end end
上の例のように、authenticate_or_request_with_http_token
のブロックでは、「トークン」と「HTTP Authorization
ヘッダーを解析したオプションを含むHash
」という2個の引数を受け取ります。このブロックは、認証が成功した場合はtrue
を返します。false
かnil
を返した場合は認証失敗です。
Railsコントローラは、HTMLページをレンダリングする代わりに、ユーザーにファイルを送信する方法を提供します。これは、クライアントにデータをストリーミングするsend_data
メソッドとsend_file
メソッドで実行できます。
send_data
メソッドは、ファイル名を指定してそのファイルの内容をストリーミングできる便利なメソッドです。
send_data
メソッドの利用方法を以下に示します。
require "prawn" class ClientsController < ApplicationController # クライアントに関する情報を含むPDFを生成し、 # 返します。ユーザーはPDFをファイルダウンロードとして取得できます。 def download_pdf client = Client.find(params[:id]) send_data generate_pdf(client), filename: "#{client.name}.pdf", type: "application/pdf" end private def generate_pdf(client) Prawn::Document.new do text client.name, align: :center text "Address: #{client.address}" text "Email: #{client.email}" end.render end end
上の例のdownload_pdf
アクションは、呼び出されたprivateメソッドで実際のPDFを生成し、結果を文字列として返します。続いてこの文字列がファイルダウンロードとしてクライアントにストリーミング送信されます。このときにクライアントで保存ダイアログが表示され、そこにファイル名が表示されます。
ストリーミング送信するファイルをクライアント側でファイルとしてダウンロードできないようにしたい場合があります。たとえば、HTMLページに埋め込める画像ファイルで考えてみましょう。このとき、このファイルはダウンロード用ではないということをブラウザに伝えるには、:disposition
オプションで"inline"を指定します。
逆のオプションは"attachment"で、こちらはストリーミングのデフォルト設定です。
サーバーのディスク上に既にあるファイルを送信するには、send_file
メソッドを使います。
class ClientsController < ApplicationController # ディスク上に生成・保存済みのファイルをストリーミング送信する def download_pdf client = Client.find(params[:id]) send_file("#{Rails.root}/files/clients/#{client.id}.pdf", filename: "#{client.name}.pdf", type: "application/pdf") end end
ファイルは、デフォルトでは4KBずつ読み出されてストリーミング送信されます。これは、巨大なファイルを一度にメモリに読み込まないようにするためです。分割読み出しは:stream
オプションでオフにすることも、:buffer_size
オプションでブロックサイズを調整することも可能です。
:type
オプションが未指定の場合、:filename
で取得したファイル名の拡張子から推測して設定されます。拡張子に該当するContent-Type
ヘッダーがRailsに登録されていない場合、application/octet-stream
が使われます。
サーバーのディスク上のファイルパスを指定するときに、(paramsやcookieなどの)ユーザーがクライアントで入力したデータを使う場合は十分な注意が必要です。クライアントから悪質なファイルパスが入力されると、開発者が意図しないファイルにアクセスされてしまうというセキュリティ上のリスクが生じる可能性を常に念頭に置いてください。
静的なファイルをRailsからストリーミング送信することは推奨されていません。ほとんどの場合、Webサーバーのpublicフォルダに置いてダウンロードさせれば済むはずです。Railsからストリーミングでダウンロードするよりも、ApacheなどのWebサーバーから直接ファイルをダウンロードする方がはるかに効率が高く、しかもRailsスタック全体を経由する不必要なリクエストを受信せずに済みます。
send_data
は問題なく利用できますが、真にRESTfulなアプリケーションを作成しているときに、ファイルダウンロード専用のアクションを別途作成する必要は通常ありません。RESTという用語においては、上の例で使われているPDFファイルのようなものは、クライアントリソースを別の形で表現したものであると見なされます。
Railsには、これに基づいた「RESTful」ダウンロードを手軽に実現するための洗練された方法も用意されています。以下は上の例を変更して、PDFダウンロードをストリーミングとして扱わずにshow
アクションの一部として扱うようにしたものです。
class ClientsController < ApplicationController # ユーザーはリソース受信時にHTMLまたはPDFをリクエストできる def show @client = Client.find(params[:id]) respond_to do |format| format.html format.pdf { render pdf: generate_pdf(@client) } end end end
これで、ユーザーは以下のようにURLの末尾に.pdf
を追加するだけで、クライアントのPDFバージョンを取得するリクエストを送信できます。
GET /clients/1.pdf
このformat
では、RailsによってMIMEタイプとして登録されている拡張機能の任意のメソッドを呼び出せます。
Railsには既に"text/html"
や"application/pdf"
などの一般的なMIMEタイプが登録されています。
Mime::Type.lookup_by_extension(:pdf) # => "application/pdf"
MIMEタイプを追加する必要がある場合は、config/initializers/mime_types.rb
ファイルでMime::Type.register
を呼び出します。たとえば、リッチテキスト形式(RTF)は以下の方法で登録できます。
Mime::Type.register("application/rtf", :rtf)
Railsの設定ファイルは起動時にしか読み込まれません。上の設定変更を反映するには、サーバーを再起動する必要があります。
Railsは、ファイル以外のデータもストリーミング送信できます。実はresponse
オブジェクトに含まれるものなら何でもストリーミング送信できます。
ActionController::Live
モジュールを使うと、ブラウザとの永続的なコネクションを作成できます。このモジュールをinclude
することで、いつでも好きなタイミングで任意のデータをブラウザに送信できるようになります。
class MyController < ActionController::Base include ActionController::Live def stream response.headers["Content-Type"] = "text/event-stream" 100.times { response.stream.write "hello world\n" sleep 1 } ensure response.stream.close end end
上のコードは、ブラウザとの間に永続的なコネクションを確立し、1秒おきに"hello world\n"
メッセージを100個ずつ送信します。
上の例にはいくつか注意点があります。
レスポンスのストリームは確実に閉じること。 ストリームを閉じ忘れると、ソケットが開きっぱなしになってしまいます。
Content-Type
ヘッダーにtext/event-stream
を設定するときは、レスポンスストリームへの書き込みの「前に」行うこと。
(response.committed?
が「truthy」な値を返したときに)レスポンスがコミット済みになっていると、以後ヘッダーに書き込みできなくなります。これは、レスポンスストリームに対してwrite
またはcommit
を行った場合に発生します。
カラオケマシンを開発していて、ユーザーが特定の曲の歌詞を表示できるようにしたいとします。Song
ごとに特定の行数の歌詞データがあり、各行には「その行を歌い終わるまであと何拍残っているか」を表すnum_beats
が記入されているとします。
歌詞を「カラオケスタイル」でユーザーに表示したいので、直前の歌詞を歌い終わってから次の歌詞を表示することになります。このようなときは、以下のようにActionController::Live
を利用できます。
class LyricsController < ActionController::Base include ActionController::Live def show response.headers["Content-Type"] = "text/event-stream" response.headers["Cache-Control"] = "no-cache" song = Song.find(params[:id]) song.each do |line| response.stream.write line.lyrics sleep line.num_beats end ensure response.stream.close end end
任意のデータをストリーミング送信できる機能は、きわめて強力なツールとなります。これまでの例でご紹介したように、任意のデータをいつでもレスポンスストリームで送信できます。ただし、以下の点についてご注意ください。
レスポンスストリームを作成するたびに新しいスレッドが作成され、元のスレッドからスレッドローカルな変数がコピーされます。スレッドローカルな変数が増えすぎたり、スレッド数が増えすぎると、パフォーマンスに悪影響が生じます。
レスポンスストリームを閉じることに失敗すると、該当のソケットが開きっぱなしになってしまいます。レスポンスストリームを使う場合は、close
を確実に呼び出してください。
WEBrickサーバーはすべてのレスポンスをバッファリングするので、ActionController::Live
ではストリーミングできません。このため、レスポンスを自動的にバッファリングしないWebサーバーを使う必要があります。
Railsのログファイルは、環境ごとにlog
フォルダの下に保存されます。ログは、デバッグ時にアプリケーションで何が起こっているかを確認するときには非常に便利ですが、production環境のアプリケーションでは顧客のパスワードのような重要な情報をログファイルに出力しないようにしておきたいのが普通でしょう。
Railsでは、ログに保存してはいけないパラメータを指定できます。
Railsアプリケーションの設定ファイルconfig.filter_parameters
には、特定のリクエストパラメータをログ出力時にフィルタで除外する設定を追加できます。
フィルタされたパラメータはログ内で[FILTERED]
という文字に置き換えられます。
config.filter_parameters << :password
ここで指定したパラメータは、ログで[FILTERED]
と出力されます。
filter_parameters
で指定したパラメータは、正規表現の「部分マッチ」によるフィルタで除外される点にご注意ください。たとえば、:passw
を指定すると、password
、password_confirmation
などもフィルタで除外されます。
Railsでは、:passw
、:secret
、:token
などのデフォルトのフィルタリストが適切なイニシャライザ(initializers/filter_parameter_logging.rb
)に追加されているので、password
、password_confirmation
、my_token
などの一般的なアプリケーションパラメータはデフォルトで除外されるようになっています。
機密性の高いURLにリダイレクトした結果をアプリケーションのログに残したくない場合があります。
設定のconfig.filter_redirect
オプションを使って、リダイレクト先URLをログに出力しないようにできます。
config.filter_redirect << "s3.amazonaws.com"
フィルタしたいリダイレクト先は、文字列か正規表現、またはそれらを含む配列で指定できます。
config.filter_redirect.concat ["s3.amazonaws.com", /private_path/]
マッチしたURLはログで[FILTERED]
という文字に置き換えられます。ただし、URL全体ではなくパラメータのみをフィルタで除外したい場合は、パラメータをフィルタするを参照してください。
コントローラへの通信をHTTPSのみに限定するには、アプリケーション環境のconfig.force_ssl
設定でActionDispatch::SSL
ミドルウェアを有効にします。
Railsには、/up
パスでアクセス可能な組み込みのヘルスチェックエンドポイントも用意されています。このエンドポイントは、アプリが正常に起動した場合はステータスコード200を返し、例外が発生した場合はステータスコード500 Server Errorを返します。
production環境では、多くのアプリケーションが、問題が発生したときにエンジニアに報告するアップタイムモニタや、ポッドの健全性を判断するロードバランサや、Kubernetesコントローラなどを用いて、状態を上流側に報告する必要があります。このヘルスチェック機能は、そうした多くの状況で利用できるように設計されています。
新しく生成されたRailsアプリケーションのヘルスチェックはデフォルトで/up
に配置されますが、config/routes.rb
でパスを自由に設定できます。
Rails.application.routes.draw do get "health" => "rails/health#show", as: :rails_health_check end
上の設定によって、GET
リクエストまたはHEAD
リクエストで/health
パスのヘルスチェックにアクセスできるようになります。
このエンドポイントは、データベースやredisクラスタなど、アプリケーションのあらゆる依存関係のステータスを反映しているわけではありません。アプリケーション固有のニーズについては、rails/health#show
を独自のコントローラアクションに置き換えてください。
ヘルスチェックでどんな項目をチェックするかの決定は、慎重に検討しましょう。場合によっては、サードパーティのサービスが不具合で停止したためにアプリケーションが不必要に再起動するような事態を招く可能性もあります。理想的には、そのような停止を適切に処理できるようにアプリケーションを設計する必要があります。
どんなアプリケーションでも、バグが潜んでる可能性や、適切に扱う必要のある例外をスローする可能性があるものです。たとえば、データベースに既に存在しなくなったリソースにユーザーがアクセスすると、Active RecordはActiveRecord::RecordNotFound
例外をスローします。
Railsのデフォルトの例外ハンドリングでは、例外の種類にかかわらず「500 Server Error」を表示します。development環境でのリクエストであれば、詳細なトレースバックや追加情報も表示されるので、これらを元に問題点を把握して対応できます。production環境でのリクエストの場合は「500 Server Error」や「404 Not Found」などのメッセージだけをユーザーに表示します。
これらのようなエラーキャッチ方法や、ユーザーへのエラー表示方法は設定でカスタマイズ可能です。Railsアプリケーションでは、さまざまなレベルの例外処理を利用できます。config.action_dispatch.show_exceptions
設定を使えば、リクエストへの応答中に発生した例外をRailsが処理する方法を制御できます。
例外のレベルについて詳しくは、Railsアプリケーションの設定項目ガイドを参照してください。
production環境のRailsアプリケーションは、デフォルトではエラー時にエラーページを表示します。
これらのエラーメッセージには、public/
フォルダ以下に置かれている静的なHTMLファイル(404.html
および500.html
)が使われるので、これらのファイルをカスタマイズすることで情報やスタイルをエラーページに追加できます。
これらのエラーページは静的なHTMLファイルなので、ERBやSCSSやレイアウトのような動的な機能は利用できません。
rescue_from
もう少し洗練された方法でエラーをキャッチしたい場合は、rescue_from
を使えます。これにより、1つ以上の例外を1つのコントローラ全体で扱うことも、そのサブクラスで扱うことも可能になります。
rescue_from
ディレクティブでキャッチ可能な例外が発生すると、ハンドラに例外オブジェクトが渡されます。
rescue_from
を使ってすべてのActiveRecord::RecordNotFound
エラーをインターセプトし、処理を行なう方法の例を以下に示します。
class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound, with: :record_not_found private def record_not_found render plain: "Record Not Found", status: 404 end end
このハンドラには、メソッドを渡すことも、:with
オプションでProc
オブジェクトを渡すことも可能です。Proc
オブジェクトを明示的な渡す代わりに、ブロックを直接渡すことも可能です。
これで先ほどよりもコードが洗練されましたが、もちろんこれだけではエラー処理は何も改良されていません。しかしこのようにすべての例外をキャッチ可能にしておくことで、今後自由にカスタマイズできるようになります。
たとえば、以下のようなカスタム例外クラスを作成すると、アクセス権を持たないユーザーがアプリケーションの特定部分にアクセスした場合に例外をスローできます。
class ApplicationController < ActionController::Base rescue_from User::NotAuthorized, with: :user_not_authorized private def user_not_authorized flash[:error] = "このセクションへのアクセス権がありません" redirect_back(fallback_location: root_path) end end class ClientsController < ApplicationController # ユーザーがクライアントにアクセスする権限を持っているかどうかをチェックする before_action :check_authorization # このアクション内で認証周りを気にする必要はない def edit @client = Client.find(params[:id]) end private # ユーザーが認証されていない場合は単に例外をスローする def check_authorization raise User::NotAuthorized unless current_user.admin? end end
rescue_from
でException
やStandardError
を指定すると、Railsの正常な例外ハンドリングが阻害されて深刻な副作用が生じる可能性があります。よほどの理由がない限り、このような指定はおすすめできません。
ActiveRecord::RecordNotFound
エラーは、production環境では常に404エラーページを表示します。この振る舞いをカスタマイズする必要がない限り、開発者がこのエラーを処理する必要はありません。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。