Hegwin.Me

疏影横斜水清浅,暗香浮动月黄昏。

Rails 7升级过程中针对cookie一系列变化的应对策略

Rotate cookies when upgrading to Rails 7

最近在进行Rails项目从 6.1 到 7 的升级,这个过程并非一蹴,而是分了好几次部署才完成最终的设置。Rails 7在带来一些新功能的同时,也带来不少 breaking changes,使得这一次的版本升级比之前的都要麻烦一些。这其中比较折腾人的就是关于 cookie的一些变化,Rails修改了默认的cookie serilaizer,以及默认的加密方式,造成了新旧cookie的不兼容,因此升级时需要格外注意,需要分步骤完成,以保证用户的最佳体验。

背景介绍

HTTP本身是无状态的,需要使用Cookie来保存状态。Cookie是存储在客户端的文本文件,它包含一个名称和值,以及其他一些可选属性,如失效日期、域和路径等。通过使用Cookie,Web应用程序可以在同一浏览器会话期间保留用户状态。

在Rails中,有多种操作cookie的方式。普通的cookie可以在 controller 中通过 cookies[:user_id] = 1 这样的方式设置,而对于签名或者加密的cookie则使用以下方式:

# Signed cookies
cookies.signed[:user_id] = 1
# Encrypted cookies
cookies.encrypted[:user_id] = 1

其中,signed cookie使用的是 secrets.secret_key_base 对 cookie 进行签名,可以保证 cookie 内容的完整性,但不能保证cookie的机密性。加密的cookie则会对内容进行加密,可以保证机密性。

cookies-in-rails.png

在controller中设置加密的cookie后,在浏览器中只能看到加密的 cookie,无法看到 cookie 具体的内容,而在controller中使用 cookies.encrypted[:user_id] 则可以获取解密后的内容。

升级到Rails 7后,cookie serializer 从 Marshal 切换到 JSON,另外,digest class由 SHA1 切换到 SHA256,这意味着之前由Rails加密的信息都会失效。这可能导致用户的登录状态或者其他信息失效,因此我们需要一些策略来使这个切换对用户无感。

Marshal和JSON是Ruby中的两种序列化方式。Marshal是Ruby原生支持的序列化方式,但它有以下缺点:

  1. 不够安全,可以通过构造恶意对象导致远程代码执行;
  2. 不够灵活,只能在Ruby环境中进行反序列化。

因此,Rails 7升级到了JSON序列化方式,提高了应用的安全性和跨语言兼容性。

针对于此,我们可以按照以下步骤进行切换:

  1. 完成 Rails 7 的升级并部署,使其他变化可以在生产环境生效,但是对 serializer 进行配置,仍然使用 Marshal 序列化:

    Rails.application.config.action_dispatch.cookies_serializer = :marshal
    
  2. 使用 Hybrid 模式的 serializer,Rails 7 提供了一个名为 :hybrid 的 serializer 配置项,可以在下一次用户访问的时候自动将过去的 cookie 转为 JSON 序列化:

    Rails.application.config.action_dispatch.cookies_serializer = :hybrid
    
  3. 在一定时间后,完全切换到 JSON 序列化,但也可以使用 :hybrid 作为最终的配置。如果打算彻底使用JSON序列化,那就可以删除 Rails.application.config.action_dispatch.cookies_serializer 采用默认 7.0 的默认配置。

需要注意的是,由于cookie不再是由Marshal序列化,这意味着不能再把一些复杂的Ruby对象放入cookies。 一般来说,使用Hash, String和 数值会是比较安全的选项。

针对 Digest Class 变化的应对方式

由于安全性考虑,Rails 7默认的digest class由SHA1切换到SHA256。

# Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
# Changing this default means invalidate all encrypted messages generated by
# your application and, all the encrypted cookies. Only change this after you
# rotated all the messages using the key rotator.
#
# See upgrading guide for more information on how to build a rotator.
# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256

这意味着之前由Rails加密的信息都会失效。因此,需要写一个cookie rotator来解决这个问题。下面是官方给出的示例代码:

# config/initializers/cookie_rotator.rb
Rails.application.config.after_initialize do
  Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
    authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
    signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt

    secret_key_base = Rails.application.secret_key_base

    key_generator = ActiveSupport::KeyGenerator.new(
      secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
    )
    key_len = ActiveSupport::MessageEncryptor.key_len

    old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
    old_signed_secret = key_generator.generate_key(signed_cookie_salt)

    cookies.rotate :encrypted, old_encrypted_secret
    cookies.rotate :signed, old_signed_secret
  end
end

上面的代码中,将加密方式从SHA1切换到SHA256,实现了对旧的cookie的rotation。

我们在app开发很少自己去手动做cookie的encryption,更常见的情况我们使用了 devise 这个gem,而 devise 会利用Rails.application.secret_key_base进行用户登录相关的cookie加密,把上面的代码放在initializer中就可以实现对旧的cookie的rotation,避免需要重新登录的情况。但也需要注意其他的gem是否有使用 ActiveSupport::KeyGenerator 相关的代码,如果有,也需要进行类似的rotation。

参考资料

  1. RailsでセッションとCookieを操作する方法

  2. Migrating Rails cookies to the new JSON serializer

  3. Rails 7 のアプリケーション設定のnew_framework_defaults_7_0項目を適当に調査

< Back