【Ruby】CORS の origin を動的に判断したい
こんにちは、okumudです。
フロントエンドのスクリプトを絡めて機能を任意サイト (別ドメイン) へ提供するにあたり、 CORS (オリジン間リソース共有: Cross-Origin Resource Sharing) の origin[1] を動的に判断させることがありました。
本記事では rack-cors gem を使用して動的に判定する方法を紹介します。
はじめに
CORS とは
ブラウザーはセキュリティ上の理由でスクリプトから異なる origin へのリクエストを制限しています。
フロントエンドのスクリプトを絡めて機能提供を行うとき、スクリプトの実行がブロックされてしまいます。
次のページに詳しい解説があるので、必要に応じて参照してください。
rack-cors gem とは
Sinatra や Ruby on Rails などに CORS のサポートを追加するためのミドルウェアです。
主な役割は、CORS ヘッダーの付与(Access-Control-Allow-Origin など) や プリフライトリクエスト (OPTIONSメソッドのリクエスト) の処理を行います。
設定方法はリポジトリのドキュメントを参照してください。
動作確認用コード
手元で確認したい場合のために検証用のコードを記載しておきます。
サンプルコード
# Gemfile
source "https://rubygems.org"
gem "puma"
gem "rackup"
gem "rack-cors"
gem "sinatra"
require "sinatra"
require "rack/cors"
# Rack::Cors を使う設定
use Rack::Cors do
allow do
origins 'http://localhost:4567', 'https://example.org' # 許可したい origin
resource '*',
headers: :any,
methods: [:get, :post, :options]
end
end
# CORS を確認するための API
get "/hello" do
content_type :json
{ message: "Hello from Sinatra with CORS!" }.to_json
end
get "/hello/:with_param" do
content_type :json
{ message: "Hello #{params[:with_param]} from Sinatra with CORS!" }.to_json
end
ローカルホストで sinatra を実行
ruby ./app.rb -p 4567
ブラウザから https://example.org または https://example.com へアクセスし、ブラウザの開発者ツール (F12) のコンソールで await fetch('http://127.0.0.1:4567/hello') を実行することで動作を確認できます。
rack-cors で origin を動的に判定する方法
rack-cors の設定で、 origins にブロックを渡し、判定した結果(true/false)を返すことで許可したドメインか、それ以外かで判定することができます。
origin を基準に判定する場合
ブロックの source に origin の URL が入ってくるので、その URL を元に判断できます。
allow do
origins do |source, env|
uri = URI.parse(source)
allowed_domains = ['localhost', 'example.org']
result = allowed_domains.include?(uri.host) # host名(完全一致)で許可する
puts "#{uri.host}: #{result}"
result # 判定した結果
end
resource '*',
headers: :any,
methods: [:get, :post, :options]
end
許可となる場合:
ブラウザで example.org へアクセスし、コンソールから await fetch('http://127.0.0.1:4567/hello') を実行します。
example.org: true
127.0.0.1 - - [27/Aug/2025:12:00:07 +0900] "GET /hello HTTP/1.1" 200 43 0.0117
→コンソールからのリクエストが成功する
拒否となる場合:
ブラウザで example.com へアクセスし、コンソールから await fetch('http://127.0.0.1:4567/hello') を実行します。
example.com: false
127.0.0.1 - - [27/Aug/2025:12:00:33 +0900] "GET /hello HTTP/1.1" 200 43 0.0041
→コンソールからのリクエストが失敗する
クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、
http://127.0.0.1:4567/hello にあるリモートリソースの読み込みは拒否されます
(理由: CORS ヘッダー ‘Access-Control-Allow-Origin’ が足りない)。
ステータスコード: 200
パスパラメーター を基準に判定する場合
ブロック引数に env が渡ってくるので、その中の PATH_INFO にリクエストされたパスが含まれます。そのパス用いて判定を行います。
allow do
origins do |source, env|
# NOTE: デコードされていないので、URLエンコードされたままとなる
puts "#{URI.parse(source).host}: #{env['PATH_INFO']}"
next unless env['PATH_INFO'] =~ %r{\A/hello/(ok|ng|%E3%81%BB%E3%81%92)}o
identifier = Regexp.last_match(1)
result = identifier == "%E3%81%BB%E3%81%92" # ほげ
puts "#{URI.parse(source).host}: #{result}"
result # 判定した結果
end
resource '*',
headers: :any,
methods: [:get, :post, :options]
end
許可となる場合:
ブラウザで example.com へアクセスし、コンソールから await fetch('http://127.0.0.1:4567/hello/ほげ') を実行します。
example.com: /hello/%E3%81%BB%E3%81%92
example.com: true
127.0.0.1 - - [27/Aug/2025:12:54:07 +0900] "GET /hello/%E3%81%BB%E3%81%92 HTTP/1.1" 200 50 0.0065
→コンソールからのリクエストが成功する
拒否となる場合:
ブラウザで example.com へアクセスし、コンソールから await fetch('http://127.0.0.1:4567/hello/ng') を実行します。
example.com: /hello/ng
example.com: false
127.0.0.1 - - [27/Aug/2025:12:56:40 +0900] "GET /hello/ng HTTP/1.1" 200 46 0.0040
→コンソールからのリクエストが失敗する
クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、
http://127.0.0.1:4567/hello/ng にあるリモートリソースの読み込みは拒否されます
(理由: CORS ヘッダー ‘Access-Control-Allow-Origin’ が足りない)。
ステータスコード: 200
これら上記の例はコード上で判定してその結果を返していますが、実際には DB 等に格納された情報をもとに判定すると思います。 DB へのアクセスには時間がかかるため、Redis 等のキャッシュを用いると、応答速度が改善され応答遅延が発生しにくくなります。
実装中に遭遇したこと
本記事の題目から少し離れますが、動的に origin を確認する仕組みを入れて動作確認した時に発生した現象を紹介します。
プレフライトが行われず、直接リクエトが飛んできた
単純リクエストとなっている可能性があります。
ブラウザ側でリクエストの内容によっては HTML4.0 の form 要素ためにプレフライト(OPTIONS メソッドでのリクエスト)が行われずにそのまま指定されたメソッドでリクエストされることがあります。
トラッキングのため navigator.sendBeacon を使うと CORS エラーとなる
navigator.sendBeacon はバックエンドに分析データを送信するために使われるもので、非同期に少量のデータを HTTP POST リクエストで送るときに使います。
rack-cors の resource の設定に credentials: true を設定してください(デフォルトで false のため)。
allow do
origins 'http://localhost:4567', 'https://example.org' # 明示的に指定
resource '*',
- headers: :any,
+ headers: 'x-domain-token', # 明示的に指定
- methods: [:get, :post, :options]
+ methods: [:get, :post, :options], # 明示的に指定
+ credentials: true # 追加
end
Beacon の仕様書上、 Content-Type: application/json のとき、credential は include となるため、Access-Control-Allow-Credentials ヘッダーを true とする必要があります。
おわりに
rack-cors gem を使うとき origins にブロックを渡すことで動的に判定することができます。
フロントエンドのスクリプトとバックエンドの処理を組み合わせてサービスを構築するときの助けになれば幸いです。
-
オリジン: ウェブコンテンツにアクセスするために使われる URL の スキーム(例: http)、 ホスト(例: example.org)、 ポート番号(例: 80) ↩︎
Discussion