Rails 7升级过程中针对cookie一系列变化的应对策略
Rotate cookies when upgrading to Rails 7
最近在进行Rails项目从 6.1 到 7 的升级,这个过程并非一蹴,而是分了好几次部署才完成最终的设置。Rails 7在带来一些新功能的同时,也带来不少 breaking changes,使得这一次的版本升级比之前的都要麻烦一些。这其中比较折腾人的就是关于 cookie的一些变化,Rails修改了默认的cookie serilaizer,以及默认的加密方式,造成了新旧cookie的不兼容,因此升级时需要格外注意,需要分步骤完成,以保证用户的最佳体验。
背景介绍
什么是cookie
HTTP本身是无状态的,需要使用Cookie来保存状态。Cookie是存储在客户端的文本文件,它包含一个名称和值,以及其他一些可选属性,如失效日期、域和路径等。通过使用Cookie,Web应用程序可以在同一浏览器会话期间保留用户状态。
Rails中的cookie是怎么操作的
在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则会对内容进行加密,可以保证机密性。
在controller中设置加密的cookie后,在浏览器中只能看到加密的 cookie,无法看到 cookie 具体的内容,而在controller中使用 cookies.encrypted[:user_id]
则可以获取解密后的内容。
Rails 7中涉及cookie的变化
升级到Rails 7后,cookie serializer 从 Marshal 切换到 JSON,另外,digest class由 SHA1 切换到 SHA256,这意味着之前由Rails加密的信息都会失效。这可能导致用户的登录状态或者其他信息失效,因此我们需要一些策略来使这个切换对用户无感。
将 cookie serializer 从 Marshal 迁移到 JSON
Marshal和JSON是Ruby中的两种序列化方式。Marshal是Ruby原生支持的序列化方式,但它有以下缺点:
- 不够安全,可以通过构造恶意对象导致远程代码执行;
- 不够灵活,只能在Ruby环境中进行反序列化。
因此,Rails 7升级到了JSON序列化方式,提高了应用的安全性和跨语言兼容性。
针对于此,我们可以按照以下步骤进行切换:
完成 Rails 7 的升级并部署,使其他变化可以在生产环境生效,但是对 serializer 进行配置,仍然使用 Marshal 序列化:
Rails.application.config.action_dispatch.cookies_serializer = :marshal
使用 Hybrid 模式的 serializer,Rails 7 提供了一个名为
:hybrid
的 serializer 配置项,可以在下一次用户访问的时候自动将过去的 cookie 转为 JSON 序列化:Rails.application.config.action_dispatch.cookies_serializer = :hybrid
在一定时间后,完全切换到 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。