はじめに
RailsアプリをAPIサーバーとして構築するには、CORS (Cross-Origin Resource Sharing)と Preflight requestの設定を行う必要があります。APIサーバーは外部からの要求に対して処理を行うため、要求元の正当性を担保しておかなければ攻撃を受けるリスクがあります。要求元の正当性を担保するための方法がCORSとPreflight requestというわけです。
本記事では、Rails (APIサーバー)にCORSとPreflight requestを設定する方法について説明します。
CORSの設定
CORSについて
CORS (Cross-Origin Resource Sharing)とはオリジン間リソース共有のことです。オリジンとは、URLのスキーム(プロトコル)、ホスト(ドメイン)、ポートを組み合わせたものです。例えばhttps://autovice.jp
というURLの場合、https://
がスキーム(プロトコル)、autovice.jp
がホスト(ドメイン)、ポートは80
(省略した場合のデフォルト値)になります。
以下はhttps://autovice.jp
と比較して同一オリジンかどうかの例です。
オリジン | 同一オリジン | 理由 |
---|---|---|
https://autovice.jp |
はい | スキーム(プロトコル)、ホスト(ドメイン)、ポートすべてが一致 |
http://autovice.jp |
いいえ | スキーム(プロトコル)が異なる |
https://api.autovice.jp |
いいえ | ホスト(ドメイン)が異なる |
https://autovice.jp:8080 |
いいえ | ポートが異なる |
オリジン間リソース共有とは、同一オリジンでない同士でリソースを共有するための設定です。例えば、https://autovice.jp
とhttps://api.autovice.jp
でリソースを共有したい場合、両者は同一オリジンではないのでCORSの設定を行う必要があります。
もしCORSの設定を行わずにリソースの共有(https://autovice.jp
からhttps://api.autovice.jp
へGET要求を行うなど)を行うと、以下のエラーが表示されます。
Access to XMLHttpRequest at 'https://api.autovice.jp' from origin 'https://autovice.jp' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
オリジン 'https://autovice.jp' からの 'https://api.autovice.jp' での XMLHttpRequest へのアクセスは、CORS ポリシーによってブロックされました。要求されたリソースに 'Access-Control-Allow-Origin' ヘッダーが存在しません。
RailsでCORSの設定を行う
RailsでCORSの設定を行うには、Gemを使う方法と使わない方法があります。どちらの方法でも簡単に設定できますが、Gemを使う方法のほうが柔軟性が高いのでおすすめです。
Gemを使う方法
rack-corsというGemを使用します。
Gemfileに以下を追記してbundle install
を行います。
Gemfile
gem 'rack-cors'
rack-corsはRailsの設定ファイルでCORSの設定を行います。以下はCORSの初期化ファイルを追加した場合の例です。
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://autovice.jp', 'http://localhost:3000'
resource '*', methods: :any, headers: :any
end
end
origins
はいくつでも設定することができます。通常、本番環境と開発環境ではオリジンが異なるので、両方設定しておくことをおすすめします。
resource
は設定したオリジン以下のパスを指定します。例えばAPIのエントリーポイントがhttps://autovice.jp/api/
だとすると、resouce
には/api/*
と設定します。resource
を複数設定したい場合は、行を追加してresource
キーワードを指定した上で別の値を設定します。また、resource
ごとに許可するHTTPメソッド(methods
)とHTTPヘッダー(headers
)を指定することができます。
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://autovice.jp', 'http://localhost:3000'
resource '/api/*',
methods: [:get, :post, :put, :patch, :delete],
headers: :any
resource '/public', methods: :get, headers: :any
end
end
Gemを使わない方法
CORSの初期化ファイルを追加し、以下を記述します。
config/initializers/cors.rb
Rails.application.configure do
config.action_dispatch.default_headers = {
'Access-Control-Allow-Origin' => 'https://autovice.jp',
'Access-Control-Request-Methods' => '*',
'Access-Control-Request-Headers' => '*'
}
end
こちらの方法だと複数のオリジンを設定できないため、本番環境と開発環境で設定を分ける必要があります。CORSの初期化ファイルではなく、各環境の設定ファイルでCORSの設定を行うように変更します。
config/emvironments/production.rb
config.action_dispatch.default_headers = {
'Access-Control-Allow-Origin' => 'https://autovice.jp',
'Access-Control-Request-Methods' => '*',
'Access-Control-Request-Headers' => '*'
}
config/emvironments/development.rb
config.action_dispatch.default_headers = {
'Access-Control-Allow-Origin' => 'http://localhost:3000',
'Access-Control-Request-Methods' => '*',
'Access-Control-Request-Headers' => '*'
}
Preflight requestの設定
Preflight requestについて
Preflight requestとは、サーバーがCORSの設定を行っており、オリジン間リソース共有ができる状態になっているかを確認するリクエストのことです。HTTPクライアントのaxiosはオリジン間リソース共有を行う際、事前にPreflight requestを送信しサーバーの状態を確認します。
Preflight requestを受け取る可能性のあるサーバーは、Preflight requestに対するレスポンスを返すようにしておく必要があります。サーバーがPreflight requestに対するレスポンスを返すようになっていない場合、以下のようなエラーが表示されます。
The request was redirected to 'https://autovice.jp/api/v1/articles', which is disallowed for cross-origin requests that require preflight
リクエストは 'https://autovice.jp/api/v1/articles' にリダイレクトされました。これはプリフライトを必要とするクロスオリジンリクエストでは許可されません。
Railsの場合は以下のようなエラーが表示されます。
ActionController::RoutingError (No route matches [OPTIONS] "/api/v1/articles")
Preflight requestはOPTIONSというHTTPメソッドで送信されてきます。サーバーがOPTIONSにマッチするルーティングを設定してないというエラー内容です。
RailsでPreflight requestの設定を行う
Preflight requestに対するレスポンスを返す処理を追加します。
app/controllers/options_request_controller.rb
class OptionsRequestController < ApplicationController
ACCESS_CONTROL_ALLOW_HEADERS = %w(Origin Content-Type Accept Authorization Token Auth-Token Email X-User-Token X-User-Email).freeze
ACCESS_CONTROL_ALLOW_METHODS = %w(GET POST PUT PATCH DELETE OPTIONS).freeze
ACCESS_CONTROL_MAX_AGE = 86_400
def response_preflight_request
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Headers'] = ACCESS_CONTROL_ALLOW_HEADERS.join(',')
response.headers['Access-Control-Allow-Methods'] = ACCESS_CONTROL_ALLOW_METHODS.join(',')
response.headers['Access-Control-Max-Age'] = ACCESS_CONTROL_MAX_AGE
head :ok
end
end
許可するHTTPメソッドやHTTPヘッダーは必要に応じて制限してください。Access-Control-Max-Age
を設定することで、同じURLに対するリクエストをキャッシュしておくことができます。
次に、OPTIONSにマッチするルーティングを追加します。
config/routes.rb
match '*path' => 'options_request#response_preflight_request', via: :options
まとめ
最近はWebアプリもマイクロサービス化しており、クライアントサーバーとAPIサーバーが分かれている構成が増えています。そのような構成では、APIサーバーでCORSとPreflight requestの設定を行っておく必要があります。
本記事を参考にして、CORSとPreflight requestの設定を行っていただければと思います。