首页ruby

cookie 原理与实现 (rails 篇)

hfpp2012发布于32 次阅读

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 条回复
暂无回复~~
喜欢

© Rails365 | 隐私条款 | 服务条款 | 粤ICP备15004902号 | 在线学员:38

Top