【Ruby】CORS の origin を動的に判断したい

に公開

こんにちは、okumudです。

フロントエンドのスクリプトを絡めて機能を任意サイト (別ドメイン) へ提供するにあたり、 CORS (オリジン間リソース共有: Cross-Origin Resource Sharing) の origin[1] を動的に判断させることがありました。
本記事では rack-cors gem を使用して動的に判定する方法を紹介します。

はじめに

CORS とは

ブラウザーはセキュリティ上の理由でスクリプトから異なる origin へのリクエストを制限しています。
フロントエンドのスクリプトを絡めて機能提供を行うとき、スクリプトの実行がブロックされてしまいます。
次のページに詳しい解説があるので、必要に応じて参照してください。
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS

rack-cors gem とは

Sinatra や Ruby on Rails などに CORS のサポートを追加するためのミドルウェアです。
主な役割は、CORS ヘッダーの付与(Access-Control-Allow-Origin など) や プリフライトリクエスト (OPTIONSメソッドのリクエスト) の処理を行います。
設定方法はリポジトリのドキュメントを参照してください。

https://github.com/cyu/rack-cors

動作確認用コード

手元で確認したい場合のために検証用のコードを記載しておきます。

サンプルコード
Gemfile
# Gemfile
source "https://rubygems.org"
gem "puma"
gem "rackup"
gem "rack-cors"
gem "sinatra"
app.rb
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 メソッドでのリクエスト)が行われずにそのまま指定されたメソッドでリクエストされることがあります。

https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS#単純リクエスト

トラッキングのため 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 のとき、credentialinclude となるため、Access-Control-Allow-Credentials ヘッダーを true とする必要があります。

おわりに

rack-cors gem を使うとき origins にブロックを渡すことで動的に判定することができます。
フロントエンドのスクリプトとバックエンドの処理を組み合わせてサービスを構築するときの助けになれば幸いです。

脚注
  1. オリジン: ウェブコンテンツにアクセスするために使われる URL の スキーム(例: http)、 ホスト(例: example.org)、 ポート番号(例: 80) ↩︎

Social PLUS Tech Blog

Discussion