为什么 rails 不断发回 Set-Cookie header?

Why is rails constantly sending back a Set-Cookie header?

我在弹性负载平衡器和 varnish 缓存方面遇到了关于 cookie 的问题,sessions 在 rails 和客户端之间混淆了。部分问题是,rails 几乎在每个请求上都添加了 "Set-Cookie" header 和 session id。如果客户端已经在发送 session_id,并且它匹配 rails 将要设置的 session_id.. 为什么 rails 会不断地告诉客户端 "oh yeah.. you're session id is ..."

总结: Set-Cookie header几乎每个回复都设置了,因为

  1. 默认的 session 存储将尝试将 session 数据写入加密的 cookie 以响应访问 session 的任何请求(从中读取或写入),
  2. 即使纯文本值没有改变,加密值也会改变,
  3. 加密发生在它到达负责检查 cookie 值是否已更改以避免冗余的代码之前 Set-Cookie headers。

Plain-text 饼干

在Rails中,ActionDispatch::Cookies中间件负责根据ActionDispatch::Cookies::CookieJar.[=33的内容编写Set-Cookie响应headers =]

正常行为是您所期望的:如果 cookie 的值与请求 Cookie header 中的值没有变化,并且到期日期没有更新,那么 Rails 将不会在响应中发送新的 Set-Cookie header。

这是由 CookieJar#[]= 中的条件处理的,它将已经存储在 cookie jar 中的值与正在写入的新值进行比较。

加密的 cookies

为了处理加密的 cookie,Rails 提供了 ActionDispatch::Cookies::EncryptedCookieJar class。

EncryptedCookieJar依赖ActiveSupport::MessageEncryptor提供加密和解密,每次调用时使用随机initialisation vector。这意味着它几乎可以保证 return 一个不同的加密字符串,即使它被赋予相同的纯文本字符串。换句话说,如果我解密我的 session 数据,然后 re-encrypt 它,我最终会得到一个与我开始时不同的字符串。

EncryptedCookieJar 做的不多:它包装了一个常规的 CookieJar,只是在数据进入时提供加密,在数据返回时提供解密。这意味着 CookieJar#[]= 方法仍然负责检查 cookie 的值是否已更改,它甚至不知道给它的值是加密的。

EncryptedCookieJar 的这两个属性解释了为什么在不更改其值的情况下设置加密 cookie 总是会导致 Set-Cookie header.

session店铺

Rails 提供不同的 session 商店。他们中的大多数将 session 数据存储在服务器上(例如在 memcached 中),但默认设置 - ActionDispatch::Session::CookieStore - 使用 EncryptedCookieJar 将所有数据存储在加密的 cookie 中。

ActionDispatch::Session::CookieStoreRack::Session::Abstract::Persisted 继承了一个 #commit_session? 方法,它决定是否应该设置 cookie。如果 session 已经加载,那么答案几乎总是“是的,设置 cookie”。

正如我们已经看到的,在 session 已加载但未更改的情况下,我们仍将以不同的加密值结束,因此 Set-Cookie header.

查看@georgebrock 的回答,了解为什么会发生这种情况。修补 rails 以更改此行为以仅在会话更改时设置 cookie 非常容易。只需将此代码放入初始化程序目录即可。

require 'rack/session/abstract/id' # defeat autoloading
module ActionDispatch
  class Request
    class Session # :nodoc:
      def changed?;@changed;end
      def load_for_write!
        load! unless loaded?
        @changed = true
      end
    end
  end
end

module Rack
  module Session
    module Abstract
      class Persisted
        private
        def commit_session?(req, session, options)
          if options[:skip]
            false
          else
            has_session = session.changed? || forced_session_update?(session, options)
            has_session && security_matches?(req, options)
          end
        end
      end
    end
  end
end