世界上最伟大的投资就是投资自己的教育

全场限时 5 折

首页Ruby
随风 · 练气

cookie 原理与实现 (rails 篇)

随风发布于3015 次阅读

1. 介绍

以前听一个朋友说过,cookie 是前端的内容,只有 js 才能设置 cookie,这样就错了。其实,任何一种后端服务器,都能操作 cookie,cookie 本身是 http 协议的内容。

比如用 rails 来操作 cookie,是类似下面这样的:

def forget_me
  cookies.delete(:remember_token)
end

def remember_me
  cookies[:remember_token] = {
    value: current_user.remember_token,
    expires: 2.weeks.from_now,
    httponly: true
  }
end

而 nginx 来设置 cookie,就更简单了,比如:

location / {
  add_header 'Set-Cookie' 'name=value';
}

只要添加一行头信息就好,健为"Set-Cookie",值为"name=value"。

这是 http 协议规定的内容,rails 在本质上也是这样实现的。

用户只要访问有这样设置过 cookie 的服务器,服务器就会把 cookie 作为响应信息传给客户端的浏览器,浏览器就把它存储起来,比如,用 chrome 的开发者工具,就可以轻易看到 cookie 的内容。

cookie 是有很多属于的,比如它是用健值对的形式存储数据的,并且有容量的限制,只能存 4096 个字节,相当于 4K 字节,namevalue都要算了,比如上面的例子中,nameavaluexx,就算 3 个字节 (a + xx)。

除此之外,还有 domain(域),path(路径),expires(过期时间),等属性。

在浏览器中,也可以用 js 把 cookie 读出来,比如document.cookie,但这个指令读不了带httponly属性的 cookie。

2. ActionDispatch::Cookies

上文有说过,在 rails 中可以轻易地设置 cookie,现在来看下它是如何实现的。

这就要说到一个中间件,名字叫ActionDispatch::Cookies

它的源码位于:https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/cookies.rb

相关的源码是这样的:

HTTP_HEADER   = "Set-Cookie".freeze

def write(headers)
  if header = make_set_cookie_header(headers[HTTP_HEADER])
    headers[HTTP_HEADER] = header
  end
end

def make_set_cookie_header(header)
  header = @set_cookies.inject(header) { |m, (k, v)|
    if write_cookie?(v)
      ::Rack::Utils.add_cookie_to_header(m, k, v)
    else
      m
    end
  }
  @delete_cookies.inject(header) { |m, (k, v)|
    ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
  }
end

果然如上文所说的一样,就是设置Set-Cookie这个头信息而已。

::Rack::Utils.add_cookie_to_header(m, k, v)rack这个库提供的方法,源码可见于:
https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L218

这个方法只不过是对 cookie 的属性做一些处理而已。

3. cookies.encrypted

因为 cookie 是存在浏览器中的,且有时候想把一些敏感的数据放到 cookie 中,又不想被轻易看到,那就可以把 cookie 数据加密一下,变成看不懂的串,再放到浏览器中,当用户请求时,只要把加密过的串解密并还原成原数据就好了,这个就是cookies.encrypted的作用。

要使用也很简单,类似下面这样:

cookies.encrypted[:discount] = 45

在浏览器中的 cookie 就是这样的:

"discount" => "R3MzTHptZnpSaXdDc3VjMm1IZThGUT09LS1zMEY4cENCT0pFV3d4VncveUdHLzZ3PT0=--f7224731a6dfa0c3736322acc945ca3c44ef0549"

可见,discount已经被加密过了。一般人就算能获得,也取不出原有的值。

在 rails 是这样实现cookies.encrypted的:

# https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/cookies.rb#L218
def encrypted
  @encrypted ||=
    if upgrade_legacy_signed_cookies?
      UpgradeLegacyEncryptedCookieJar.new(self)
    else
      EncryptedCookieJar.new(self)
    end
end

相关的源码有这么几处:

也就是说,总有一个算法,对原有值带上 salt 进行加密,且还能解密还原值。

在 ActionDispatch::Cookies 中间件中相关的加密算法是这样的:

def initialize(parent_jar)
  super

  if ActiveSupport::LegacyKeyGenerator === key_generator
    raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
      "Read the upgrade documentation to learn more about this new config option."
  end

  secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
  sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
  @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end

先来看看ActiveSupport::MessageEncryptor的用法。

官方有一个例子是这样的:

salt  = SecureRandom.random_bytes(64)
key   = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
crypt = ActiveSupport::MessageEncryptor.new(key)                       # => #<ActiveSupport::MessageEncryptor ...>
encrypted_data = crypt.encrypt_and_sign('my secret data')              # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
crypt.decrypt_and_verify(encrypted_data)                               # => "my secret data"

其实就演示了加密和解密的操作,最主要的有两个地方ActiveSupport::KeyGenerator.new('password')和那个salt

key_generator这个东西其实是跟config/secrets.ymlsecret_key_base这个 key 有关系,而那个 salt,其实就是encrypted_cookie_salt的内容,它的值是encrypted cookie,当然,也可以在配置文件中设置这个值。

key_generator相关的源码在这里:

# https://github.com/rails/rails/blob/d50d7094247aad5005cd1b47258ddf338b0dddd7/railties/lib/rails/application.rb#L165
def key_generator
  # number of iterations selected based on consultation with the google security
  # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
  @caching_key_generator ||=
    if secrets.secret_key_base
      unless secrets.secret_key_base.kind_of?(String)
        raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`"
      end
      key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
      ActiveSupport::CachingKeyGenerator.new(key_generator)
    else
      ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
    end
end

也就是说,secret_key_base 是保存在服务器的,不被泄露的,得到了浏览器加密过的 cookie,也是解密不了的。如果secret_key_base不小心泄露了,而encrypted_cookie_salt的值没作修改的情况下 (一般都不会改),那就会有安全问题,就可以通过secret_key_base破解出原来的值。

不旦如此,secret_key_base泄露了,不止 cookie 出问题,连 session 也同样有问题。

这是下节要讲的内容:session 原理与实现 (rails 篇)

完结。

本站文章均为原创内容,如需转载请注明出处,谢谢。

0 条回复
暂无回复~~
喜欢
统计信息
    学员: 29066
    视频数量: 1973
    文章数量: 489

© 汕尾市求知科技有限公司 | Rails365 Gitlab | Qiuzhi99 Gitlab | 知乎 | b 站 | 搜索

粤公网安备 44152102000088号粤公网安备 44152102000088号 | 粤ICP备19038915号

Top