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です