Railsでsubdomainを使う際にtld_lengthを設定する

Railsでサブドメインを使う際の注意点です。

RailsCastに書いてある通りに設定すればsubdomainが使えるのですが、Railsのデフォルトでは、.comや.orgなどのgTLDを前提に設定されています。

つまり、example.comのような場合では

  • ja.example.com
  • en.example.com

のようにうまくいくのですが、example.co.jpのような場合、

  • ja.co.jp
  • en.co.jp

のようになってしまったりします。

このあたりは、action_dispatch/http/url.rbで定義されており、以下のような実装になっています。

module ActionDispatch
  module Http
    module URL 
      mattr_accessor :tld_length
      self.tld_length = 1 

      class << self
        def extract_domain(host, tld_length = @@tld_length)
          return nil unless named_host?(host)
          host.split('.').last(1 + tld_length).join('.')
        end 

        def extract_subdomains(host, tld_length = @@tld_length)
          return [] unless named_host?(host)
          parts = host.split('.')
          parts[0..-(tld_length+2)]
        end 

        def extract_subdomain(host, tld_length = @@tld_length)
          extract_subdomains(host, tld_length).join('.')
        end

なんかお粗末な実装ですが、要するにtld_lengthを設定すればOKですね。
developmentとproductionで違うのが普通だと思うので、以下のようにenvironmentsかinitializersで設定します。
initializersで設定する場合、他のinitializersとの依存に注意して、早めに設定しておくのが無難です(000_のprefixを付けるなど)。

# config/environments/production.rb
config.action_dispatch.tld_length = 2

# またはinitializers以下で
ActionDispatch::Http::URL.tld_length = 2

これは、たとえば自社サービスで、自社ドメインのサブドメインがサービスのメインドメインになる場合も適用できます。
たとえば

  • ja.myservice.mycompany.co.jp
  • en.myservice.mycompany.co.jp

の場合、tld_lengthは3に設定すればOKです。

cookieを共通にする

デフォルト設定では、ja.example.comとen.example.comでは別々のcookieが発行されます。
共通にしたい場合、RailsCastにあるように、以下の設定をします。

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, :key => '_mysession', :domain => :all

cookieを共通にする(自社サブドメイン)

上記設定は、ja.example.comやja.example.co.jpならうまく動くのですが、ja.myservice.mycompany.comやja.myservice.mycompany.orgでは期待に添わない動作をします(*.mycompany.comのcookieが吐かれます)。
自社の他のサービスにまでcookieが送信される、ちょっと怖いcookieになってしまいました。

cookieのhostを出力する部分の実装は以下のようになっています。

# action_dispatch/middleware/cookies.rb
# 一部省略
module ActionDispatch
  class Cookies
    class CookieJar #:nodoc:
      DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/

      def handle_options(options) #:nodoc:
        options[:path] ||= "/"

        if options[:domain] == :all
          # if there is a provided tld length then we use it otherwise default domain regexp
          domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP

          # if host is not ip and matches domain regexp
          # (ip confirms to domain regexp so we explicitly check for ip)
          options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
            ".#{$&}"
          end
        elsif options[:domain].is_a? Array
          # if host matches one of the supplied domains without a dot in front of it
          options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] }
        end
      end
    end
  ene
end

また悲しい気分になる実装ですね。共通化しろよ…
つまり、DOMAIN_REGEXPにより

  • localhost
  • example.localhost
  • example.info
  • example.jp
  • example.com
  • example.co.jp
  • example.com.jp

などは認識されます。ccTLDは2文字、ccSLDは2~3文字なので、一般的にはOKということですね。

ja.myservice.mycompany.comの場合、myservice.mycompany.com部分を「ドメイン」として認識して欲しいのですが、Railsの実装ではmycompany.com部分が「ドメイン」として認識され、「.mycompany.com」のcookieが吐かれてしまいます。

これを解決するには、mycompany.com部分を「TLD」と認識させるのが良さそうです。TLDではないですが、Railsの実装に手を入れなくてすむのはそれしかなさそうです。

ということで、tld_length=2をオプションで渡したくなりますが、実装をよく見てください。

domain_regexp = options[:tld_length] ? 
  /([^.]+\.?){#{options[:tld_length]}}$/ :
  DOMAIN_REGEXP

はい、間違ってますね。tld_lengthという名前ですが、実際は+1した値を渡さないと正しく動作しません。

ということで、以下のようにしましょう。

# config/initializers/session_store.rb
options = {
  :key => '_mysession',
  :domain => :all,
  :tld_length => ActionDispatch::Http::URL.tld_length + 1
}
Rails.application.config.session_store :cookie_store, options

production.rbなどの設定と重複しないですむように、ActionDispatch::Httpを参照するようにしました。

なお、間違って既に*.mycompany.comのcookieが吐かれてしまっている場合、衝突してデバッグがめんどくさくなるので、いったんブラウザcookieを削除することをおすすめします。

おまけ:手動でサブドメイン対応のcookieを出力する

これまでで、サブドメイン間で共通のsessionを成立させることができました。

ところで、sessionではなくcookieに生の値を入れたいこともあると思います。
たとえば、OAuthで外部サイトにリダイレクトして処理する際に、ライブラリ側が勝手にセッションIDを更新する場合があり、値の引き回しをしたい…などです。

この場合、普段は

cookies['message'] = 'hello'

のようにしますが、このcookieはサブドメイン間で共通になりません。

この場合は、手動で設定してやりましょう。

tld_length = ActionDispatch::Http::URL.tld_length
cookies['message'] = 
  { value: 'hello', domain: :all, tld_length: tld_length + 1 }

長かったですが、これで、サブドメインを自由に設定できるようになりました。

本記事の対象バージョンはRails 3.2.8です

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

baba

ゆとりプログラマー。 高校時代から趣味でプログラミングを初め、そのままコードを書き続けて現在に至る。慶應義塾大学環境情報学部(SFC)卒業。BPS設立初期に在学中から参加している最古参メンバーの一人。得意分野はWeb全般、Ruby on Rails、Androidアプリケーションなど。最近はBlinkと格闘中。軽度の資格マニアで、情報処理技術者試験(高度10区分)などを保有。

babaの書いた記事

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ