发布于 2026-01-06 0 阅读
0

揭秘 Rails 中的 cookie 安全:Rails 中的 6 种 cookie 类型:纯文本 cookie、加密 cookie 和 cookie 的生命周期

揭秘 Rails 6 中的 cookie 安全

Rails 中的 Cookie 类型

纯文本 Cookie

加密 Cookie

Cookie 的生命周期

这篇文章最初发表在我的博客上。

我还根据这篇文章做了一次演讲。点击这里查看

几乎所有现代 Web 应用程序都会使用Cookie。它们用于各种用途,例如方便用户身份验证和存储用户偏好设置。由于 Cookie 的应用如此广泛,像 Rails 这样的全栈开发框架拥有简单便捷的 API来管理 Cookie 也就不足为奇了。

在这篇文章中,我将介绍 Ruby on Rails 支持的不同类型的 cookie 以及它们在底层的工作原理。

Rails 中的 Cookie 类型

Rails 支持存储 3 种类型的 cookie:

  • 纯文本:用户可以查看和更改这些 cookie。

  • 已签名:已签名 cookie 看起来像乱码,但用户可以轻松解码,尽管由于经过加密签名,无法修改。 

  • 加密:加密 cookie 无法被用户解码(至少不容易解码),也无法被修改,因为它们在解密时会进行身份验证。

纯文本 Cookie

纯文本 cookie 应谨慎使用,并尽量少用。用户可以查看并将其值更改为任何值,而我们的应用程序却毫不知情。纯文本 cookie 的一个典型用例是存储是否已向用户显示欢迎消息。

您只需在控制器操作中编写一行代码即可设置此类 cookie:



def show
  cookies[:welcome_message_shown] = "true"
end


Enter fullscreen mode Exit fullscreen mode

这行代码会在响应中添加一个Set-Cookie HTTP 头部,其值为 `cookie` welcome_message_shown=true。浏览器收到响应后,会将该 cookie 存储起来,并在后续的每个请求中将其作为头部发送。您可以在浏览器开发者工具的“存储”选项卡中查看该 cookie。

使用浏览器的开发者工具检查 cookie 的值

双击并修改字段即可更改 cookie 的值。在这种情况下,更改值无关紧要,因为最坏的情况也只是再次向用户显示欢迎消息。对于任何敏感信息,都应使用签名加密的cookie。

签名 Cookie

签名 Cookie 旨在存储用户可查看但不可修改的信息。用户 ID或用户偏好设置等值是签名 Cookie 的理想选择。

已签名 cookie 的值会与一些元数据一起序列化,然后再进行编码和签名。默认序列化器是 `<default_serializer_name>`,但可以在目录下的文件JSON中更改cookies_serializer.rbconfig/initializers

Rails 在底层使用ActiveSupport::MessageVerifierAPI 对 cookie 数据进行编码和签名。

这些 cookie 也可以在 JavaScript 中读取(稍后会演示),因此它们是将用户特定数据从数据库发送到 JavaScript 应用程序的好方法。

存储签名 cookie 与存储纯文本 cookie 一样简单:



def show
  cookies.signed[:user_id] = "42"
end


Enter fullscreen mode Exit fullscreen mode

这样一来,得到的饼干看起来就像肉眼无法辨认的乱码。



"eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79"


Enter fullscreen mode Exit fullscreen mode

这个字符串由两部分组成,它们之间用逗号分隔--。第一部分是一个Base64 编码的JSON 对象,其中包含我们存储的值;第二部分是一个加密生成的摘要。当 Rails 收到一个签名 cookie 时,它​​会将 cookie 的值与摘要进行比较,如果不匹配,则 cookie 的值将被置为nil空。这就是为什么用户无法修改签名 cookie 的原因。

解码签名 cookie

可以使用以下 Ruby 代码解码已签名的 cookie:



cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79"
cookie_value = cookie.split("--").first
cookie_value = URI.unescape(cookie_value)
cookie_payload = JSON.parse Base64.decode64(cookie_value)


Enter fullscreen mode Exit fullscreen mode

上述代码通过分割 cookie 值来提取Base64 编码的 JSON 对象--。然后,它对该值进行转义、解码并解析,最终得到一个Hash类似这样的 JSON 对象:



{ 
  "_rails"=> {
    "message"=>"IjQyIg==", 
    "exp"=>nil, 
    "pur"=>"cookie.user_id"
  }
}


Enter fullscreen mode Exit fullscreen mode

这里唯一相关的属性是message. exp(过期时间)和(用途)是解码和验证期间pur使用的值。ActiveSupport::MessageVerifier

message也是一个Base64 编码的 JSON 对象,所以我们用与上面相同的方式对其进行解码:



decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"]
stored_value = JSON.parse decoded_stored_value
# => "42"


Enter fullscreen mode Exit fullscreen mode

由于它是以Base64 编码的 JSON 对象message形式存储的,我们可以将任何可序列化的 JSON 对象存储在签名 cookie 中;它不必是字符串。但是,要存储其他类型的对象,则需要将其放置在带有特定键的cookie 中Hashvalue



def show
  cookies.signed[:preferences] = { 
    value: {
      use_dark_mode: true
    }
  }
end


Enter fullscreen mode Exit fullscreen mode

使用 JavaScript 解码签名 cookie

上面这段用于解码签名 cookie 的 Ruby 代码可以非常轻松地转换成 JavaScript。因此,如果您需要在客户端使用存储在签名 cookie 中的信息,完全可以做到!



let cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79"
let cookie_value = unescape(cookie.split("--")[0])
let cookie_payload = JSON.parse(atob(cookie_value))

let decoded_stored_value = atob(cookie_payload._rails.message)
let stored_value = JSON.parse(decoded_stored_value)

console.log(stored_value)
// => "42"


Enter fullscreen mode Exit fullscreen mode

摘要是如何计算的

签名 cookie 的后半部分是摘要,用于验证其有效性。它使用 OpenSSL 计算,默认采用哈希函数。您可以通过在配置文件中SHA1设置来更改哈希函数config.action_dispatch.signed_cookie_digestapplication.rb

哈希函数secret除了需要哈希的数据外,还需要一个参数。该参数secret使用 OpenSSL 计算基于secret_key_base文件中的密钥credentials.yml另一个称为盐值的字符串。默认情况下,盐值为“signed cookie”,但可以通过设置来更改config.action_dispatch.signed_cookie_salt

按照 Rails 源代码中使用的相同方法,我们可以使用以下代码计算摘要:



cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79"
cookie_value = URI.unescape(cookie.split("--").first)

secret = Rails.application.secret_key_base
key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret, "signed cookie", 1000, 64)
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get("SHA1").new, key, cookie_value)
# => "94afbf4575daf37313f40d6342a994a5e1719d79"

digest == cookie.split("--").second
# => true


Enter fullscreen mode Exit fullscreen mode

如您所见,使用 OpenSSL 计算的摘要与 cookie 的摘要部分相匹配。因此,如果攻击者试图修改 cookie 中的数据,摘要将不再匹配,Rails 将会丢失cookie 的内容。攻击者只有在知道 cookie 的参数盐值nil的情况下才能计算出有效的摘要;这就是为什么保护这些值至关重要。secret_key_base

实际上,Rails 使用OpenSSLActiveSupport::KeyGeneratorActiveSupport::MessageVerifier抽象化 OpenSSL 函数。不过,为了清晰起见,我在上面的演示中直接使用了 OpenSSL。这些加密函数可以在任何编程语言中使用,用于编码和解码 Rails cookie;因此,即使你的基础设施中存在并非使用 Rails 编写的服务,你仍然可以轻松地使用 Rails cookie 中的数据。

加密 Cookie

存储在 Cookie 中的任何敏感数据都应始终加密。remember_token应用程序通常使用 Cookie 来保持用户登录状态,即使用户关闭浏览器。此类信息与用户的密码一样敏感,因此是应该存储在加密 Cookie 中的一个很好的例子。

加密 cookie 的序列化方式与签名 cookie 相同,并且使用ActiveSupport::MessageEncryptor(底层使用 OpenSSL)进行加密。 

我们来创建一个加密 cookie,看看它长什么样:



def show
  cookies.encrypted[:remember_token] = "token"
end


Enter fullscreen mode Exit fullscreen mode

这将设置一个类似如下的 cookie:



"aDkxgmW4kaxoXBGnjxAaBY7D47WUOveFdeai5kk2hHlYVqDo7xtzZJup5euTdH5ja5iOt37MMS4SVXQT5RteaZjvpdlA%2FLQi7IYSPZLz--2A6LCUu%2F5AsLfSez--QD%2FwiA2t8QQrKk6rrROlPQ%3D%3D"


Enter fullscreen mode Exit fullscreen mode

如上所示,加密 cookie 分为三个部分,中间用逗号分隔--,而不是像签名 cookie 那样分为两部分。第一部分是加密数据。第二部分称为初始化向量,它是加密算法的随机输入。第三部分是认证标签,类似于签名 cookie 的摘要。这三个部分都经过Base64 编码

默认情况下,Cookie使用 256 位密钥的AES算法,以Galois/Counter 模式进行加密aes-256-gcm可以通过设置将其更改config.action_dispatch.encrypted_cookie_cipher为任何有效的OpenSSL::Cipher算法。

解密加密的 Cookie

该cookie 使用用于计算签名 cookie 摘要的密钥相同的方式生成的密钥进行加密。因此,我们需要应用程序能够解密该 cookie。默认情况下,盐值为“已认证加密 cookie”,但可以通过设置进行更改secret_key_baseconfig.action_dispatch.authenticated_encrypted_cookie_salt

Rails 源代码为参考,我们可以按如下方式解密 cookie:



cookie = "aDkxgmW4kaxoXBGnjxAaBY7D47WUOveFdeai5kk2hHlYVqDo7xtzZJup5euTdH5ja5iOt37MMS4SVXQT5RteaZjvpdlA%2FLQi7IYSPZLz--2A6LCUu%2F5AsLfSez--QD%2FwiA2t8QQrKk6rrROlPQ%3D%3D"
cookie = URI.unescape(cookie)
data, iv, auth_tag = cookie.split("--").map do |v| 
  Base64.strict_decode64(v)
end
cipher = OpenSSL::Cipher.new("aes-256-gcm")

# Compute the encryption key
secret_key_base = Rails.application.secret_key_base
secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len)

# Setup cipher for decryption and add inputs
cipher.decrypt
cipher.key = secret
cipher.iv  = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""

# Perform decryption
cookie_payload = cipher.update(data)
cookie_payload << cipher.final
cookie_payload = JSON.parse cookie_payload
# => {"_rails"=>{"message"=>"InRva2VuIg==", "exp"=>nil, "pur"=>"cookie.remember_token"}}

# Decode Base64 encoded stored data
decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"]
stored_value = JSON.parse decoded_stored_value
# => "token"


Enter fullscreen mode Exit fullscreen mode

上面的代码应该很清楚地展示了如何使用 OpenSSL 解密 cookie。由于secret_key_base解密 cookie 需要用到密钥,而密钥又包含高度敏感的信息,因此绝对不能将其发送给客户端,所以加密的 cookie 也绝对不能在 JavaScript 应用程序中解密。

Cookie 的生命周期

默认情况下,cookie 会随浏览器的“会话”一起过期。这意味着当用户关闭浏览器时,所有过期日期为 2023 年 12 月 1 日的 cookie 都Session将被删除。 

通过指定过期日期,可以使 Cookie 在会话之间保持有效:



def show
  cookies[:welcome_message_shown] = {
    value: "true",
    expires: 7.days
  }
end


Enter fullscreen mode Exit fullscreen mode

Rails 还有一种特殊的永久cookie 类型,其过期日期设置为 20 年后。



def show
  cookies.permanent[:welcome_message_shown] = "true"
end


Enter fullscreen mode Exit fullscreen mode

签名和加密的 cookie 可以与永久类型的cookie 链接在一起,以便在浏览器会话之间持久保存。



def show
  cookies.signed.permanent[:user_id] = "42"
end


Enter fullscreen mode Exit fullscreen mode


def show
  cookies.encrypted.permanent[:remember_token] = "token"
end


Enter fullscreen mode Exit fullscreen mode

特殊会话 cookie

Rails 提供了一种特殊的 cookie,称为会话 cookie (session cookie)。顾名思义,它的过期时间是Session。这是一个加密 cookie,用于存储用户数据Hash。它非常适合存储身份验证令牌和重定向地址等信息。Rails 将数据存储Flash在会话 cookie 中。

数据可以像普通 cookie 一样存储在会话 cookie 中:



def create
session[:auth_token] = "token"
end

Enter fullscreen mode Exit fullscreen mode




结论

我希望这篇文章能让你更好地了解 cookie 以及MessageVerifierAPI MessageEncryptor,它们除了 cookie 之外还有一些很棒的应用。 

我并非密码学专家,本文中的所有内容都是通过研究 Rails 源代码得出的。因此,如果有什么不清楚的地方或者我哪里说错了,请留言告诉我!

文章来源:https://dev.to/ayushn21/demystifying-cookie-security-in-rails-6-1j2f