rubyhfpp2012发表于*编辑于

1. 介绍

上一篇文章cookie原理与实现(rails篇)有说过在rails是如何存取cookie的,而这一篇文章来讲讲rails是如何操作session的。

默认情况下,session是保存在cookie中的。这又是怎么回事呢。众所周知,cookie是存在客户端浏览器中的,把session放在cookie中,可以吗?答案当然是可以。那安全吗?还算安全。就算有人得到了那cookie,它也解密不了,因为存在cookie中的session是通过对称加密处理的,没有密钥是得不到原串的。

rails中关于存储session的配置是这样的:

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: '_rails365_session'

所以的session信息,都会存放到cookies中,且key为_rails365_session

2. ActionDispatch::Session::CookieStore

实现把session存放到cookie中的功能都依靠这个中间件:ActionDispatch::Session::CookieStore

来看下实现这个功能相关的源码:

# https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb

# 得到非加密的session数据,就是原来的session数据
def unpacked_cookie_data(req)
  req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
    v = stale_session_check! do
      if data = get_cookie(req)
        data.stringify_keys!
      end
      data || {}
    end
    req.set_header k, v
  end
end

# session数据包含session_id,值为随机码
def write_session(req, sid, session_data, options)
  session_data["session_id"] = sid
  session_data
end

# 把session数据存储到cookie中
def set_cookie(request, session_id, cookie)
  cookie_jar(request)[@key] = cookie
end

def get_cookie(req)
  cookie_jar(req)[@key]
end

来看看unpacked_cookie_data的实现,它是得到解密后session的数据。

比如下面这样:

env["action_dispatch.request.unsigned_session_cookie"]
{
     "session_id" => "760be4b1069ab0c80ccade6d36f00355",
    "_csrf_token" => "QdtMjYciHnF8XqSCe0xr8nHo3N5pQdhNeKWhe5ZxOC4=",
          "admin" => true
}

或者

session
{
     "session_id" => "760be4b1069ab0c80ccade6d36f00355",
    "_csrf_token" => "QdtMjYciHnF8XqSCe0xr8nHo3N5pQdhNeKWhe5ZxOC4=",
          "admin" => true
}

上面的数据都可以在controller里得到。

3. rack/session/cookie.rb

也就是说,从客户端传给服务器的加密过的cookie已经被先被解密了。

是的,已经先被处理了。

cookie_store.rb的源码,有一行是这样的:

require 'rack/session/cookie'

是rack程序,我们找到其源码

# https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb

def unpacked_cookie_data(request)
  request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
    session_data = request.cookies[@key]

    if @secrets.size > 0 && session_data
      digest, session_data = session_data.reverse.split("--", 2)
      digest.reverse! if digest
      session_data.reverse! if session_data
      session_data = nil unless digest_match?(session_data, digest)
    end

    request.set_header(k, coder.decode(session_data) || {})
  end
end

def write_session(req, session_id, session, options)
  session = session.merge("session_id" => session_id)
  session_data = coder.encode(session)

  if @secrets.first
    session_data << "--#{generate_hmac(session_data, @secrets.first)}"
  end

  if session_data.size > (4096 - @key.size)
    req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
    nil
  else
    session_data
  end
end

def generate_hmac(data, secret)
  OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
end

可见,不仅解密,连加密也是在那个rack程序中实现的。

使用的加密算法是最常用的OpenSSL::HMAC算法。

这个算法使用的secret或salt是@secrets.first@secrets是在下面这里定义的:

# https://github.com/rails/rails/blob/d50d7094247aad5005cd1b47258ddf338b0dddd7/railties/lib/rails/application.rb#L383
def secrets
  @secrets ||= begin
    secrets = ActiveSupport::OrderedOptions.new
    yaml = config.paths["config/secrets"].first
    if File.exist?(yaml)
      require "erb"
      all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}
      env_secrets = all_secrets[Rails.env]
      secrets.merge!(env_secrets.symbolize_keys) if env_secrets
    end

    # Fallback to config.secret_key_base if secrets.secret_key_base isn't set
    secrets.secret_key_base ||= config.secret_key_base
    # Fallback to config.secret_token if secrets.secret_token isn't set
    secrets.secret_token ||= config.secret_token

    secrets
  end
end

其实就是加载config/secrets.yml文件取得里面的secret_key_base参数的值,作为secret。

也就是说,session以cookie的方式存储的加密还是会依赖于secret_key_base参数的值,所以secret_key_base是不能泄漏的。

完结。