同じ名前のcookieが複数ある場合の仕様
1つのRailsアプリケーションを複数のsubdomainで動かしているとします。デフォルトでsession storeはcookieなのでブラウザは以下のようなcookieを保持している状態になります。
_my_app=ABC; domain=www.example.com
Subdomain間でログイン状態を維持したくなったので、subdomain間で同じcookieを送る設定に変更したとします。
module MyApp
class Application < Rails::Application
config.session_store :cookie_store, key: "_my_app", domain: :all
end
end
変更後、sessionに値を書き込んでSet-Cookie: _my_app=GHI; domain=.example.com
を返却したとします。するとブラウザは以下2つのcookieを保持することになります。
_my_app=ABC; domain=www.example.com
_my_app=EFG; domain=.example.com
この状態でブラウザが送信するリクエストのヘッダーはCookie: _my_app=ABC; _my_app=GHI
になります。同じ名前のcookieが複数ありますが、RailsからするとABC
の方のみしか見えません。結果としてsessionに書き込んだ値は全て無視されてしまいます。
# https://github.com/rack/rack/blob/64ad26e3381da2ce1853638a2c4ea241c2ad3729/lib/rack/utils.rb#L223-L231
def parse_cookies_header(value)
return {} unless value
value.split(/; */n).each_with_object({}) do |cookie, cookies|
next if cookie.empty?
key, value = cookie.split('=', 2)
# unless cookies.key?してるので、2つ目以降は無視される。
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
end
end
対応としては単にcookie storeのkeyを変更すれば十分です。ただ、ブラウザがcookieを組み立てる際の順番が気になります。先ずはRFC1で仕様を確認してみましょう。
2 . The user agent SHOULD sort the cookie-list in the following order:
- Cookies with longer paths are listed before cookies with shorter paths.
- Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.
Pathが長い方を優先し、同じ場合は作成時刻が早い方を優先するとされています。最後に、この仕様通りに実装されているかChromiumのコードを覗いたところ、確かに仕様通りに実装されている事が確認出来ました。
// https://github.com/chromium/chromium/blob/36b5627a5247893ed3cbfbc2fd569dc406b0b570/net/cookies/cookie_monster.cc#L580-L588
bool CookieMonster::CookieSorter(const CanonicalCookie* cc1,
const CanonicalCookie* cc2) {
// Mozilla sorts on the path length (longest first), and then it sorts by
// creation time (oldest first). The RFC says the sort order for the domain
// attribute is undefined.
if (cc1->Path().length() == cc2->Path().length())
return cc1->CreationDate() < cc2->CreationDate();
return cc1->Path().length() > cc2->Path().length();
}