世界上最伟大的投资就是投资自己的教育
cookie 原理与实现 (rails 篇)
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 字节,name
和value
都要算了,比如上面的例子中,name
为a
,value
为xx
,就算 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
相关的源码有这么几处:
- https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/cookies.rb#L218
- https://github.com/rails/rails/blob/29be3f5d8386fc9a8a67844fa9b7d6860574e715/activesupport/lib/active_support/message_encryptor.rb#L45
- https://github.com/rails/rails/blob/7bf9bd9e9a05d0fdbaa87b04e771ccc4e22f491e/activesupport/lib/active_support/message_verifier.rb#L30
也就是说,总有一个算法,对原有值带上 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.yml
的secret_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 篇)。
完结。
本站文章均为原创内容,如需转载请注明出处,谢谢。
© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn
粤公网安备 44152102000088号 | 粤ICP备19038915号
Top