Secret Token in Rails

记得以前在读Ruby on Rails Tutorial的时候,里面提到了下面的内容:

Rails 使用安全权标来加密会话。我们要把硬编码的字符串改为动态生成的。并且修改 .gitignore 文件,不把 .secret 纳入仓库

然后有下面的一段代码,config/initializers/secret_token.rb

require 'securerandom'

def secure_token
  token_file = Rails.root.join('secret')

  if File.exist?(token_file)
    File.read(token_file).chomp
  else
    token = SecureRandom.hex(64)
    File.write(token_file, token)
    token
  end
end

SampleApp::Application.config.secret_key_base = secure_token

当时读的时候对这段代码倒是完全能理解,但是对于secure_token在 Rails 中的用途则是一无所知,昨天突然想到了这个然后查阅了一些资料之后有了一定的理解。

Your secret_token is used for verifying the integrity of you app's session cookies

这时官方文档中对secret_token的解释,看来它是用来验证我们的会话 cookies 的合法性的,那么具体的一个过程是怎么样的呢? 首先我们来看看我们一般访问的 Rails 的网站中session cookie的样子

_MyApp_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTcyZTAwMmRjZTg2NTBiZmI0M2UwZmY0MjEyNGJjODBhBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWhmYTBKSGQwYVQxRlhnTFZWK2FEZEVhbEtLbDBMSitoVEo5YU4zR2dxM3M9BjsARg%3D%3D--dc40a55cd52fe32bb3b84ae0608956dfb5824689

=后面的部分就是cookie value,它被--分为了两部分,第一部分是一个Base64的字符串,它的原始值我们可以在 controllers 中获取到,比如我的最开始的就是这样

{"_csrf_token"=>"d09DmGvifqQQOL10CRuCmkmjcfKqFRSYiRN2gMvGDsI="}

我们还可以再忘里面添加内容,比如session['foo'] = 'bar'之类。第二部分也就是签名部分,它用于确保我们的 session 的合法性。

Session hash

因为目前我们得到的还是一个 hash,所以我们还需要对它进一步处理。

session_hash = {"_csrf_token"=>"d09DmGvifqQQOL10CRuCmkmjcfKqFRSYiRN2gMvGDsI="}
# Serialize hash
marshal_dump = Marshal.dump(session_hash)
# => "\x04\b{\x06I\"\x10_csrf_token\x06:\x06EFI\"1d09DmGvifqQQOL10CRuCmkmjcfKqFRSYiRN2gMvGDsI=\x06;\x00F"

# Base64 encode this dump
unescaped_cookie_value = Base64.encode(marshal_dump)
# => "BAh7BkkiEF9jc3JmX3Rva2VuBjoGRUZJIjFkMDlEbUd2aWZxUVFPTDEwQ1J1\nQ21rbWpjZktxRlJTWWlSTjJnTXZHRHNJPQY7AEY=\n"

# Escape line breaks & troublesome characters
escaped_cookie_value = CGI.escape(unescaped_cookie_value).gsub("%0A", "")
# => "BAh7BkkiEF9jc3JmX3Rva2VuBjoGRUZJIjFkMDlEbUd2aWZxUVFPTDEwQ1J1Q21rbWpjZktxRlJTWWlSTjJnTXZHRHNJPQY7AEY%3D"

至此,我们完成了 session cookie 的第一部分。

Signature

第二部分也是最关键的部分,由于cookie是存在于客户端的,因此客户端可以传来任意的字符串,而这也为一些非法攻击埋下了隐患。这时就是secret_token派上用场的时候了。第二部分其实就是通过对secret_token与第一部分生成的escape_cookie_value进行哈希生成的一个签名的字符串。

# Calculate the signature using the HMAC digest of the secret_token and the escaped cookie value. Replace %3D with equal sign
cookie_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret_token, escaped_cookie_value.gsub("%3D", "="))

这样即使攻击者伪造篡改用户的cookie,但是由于由于无法获得secret_token,所以也就无法获得正确的签名因而导致 cookie 无效。这有效的确保了 Rails 程序的安全性。

而在 Rails 4.1 之前的版本中,secret_token都是直接写在config/initializers/secret_token.rb文件中的。而这个文件默认是会加载到 Git 的仓库中,所以一旦被网络上不怀好意的人看到了,将会造成很不好的影响。因此,Ruby on Rails tutorial书中建议secret_token采用动态生成的方式,将它单独写到.secret的文件中,并且纳入到.gitignore文件中,这样就能有效的对secret_token进行保密。

在 Rails 4.1 中,新加入了config/secrets.yml文件,这个文件按通常 Rails 程序的配置方式分为developmenttestproduction,前两个环境中的secret_token都是直接写死的,对于开发测试模式来说这也是完全合理的。而production模式则是这样:

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

也就是把secret_token直接加入到了环境变量中,当我们在部署 Rails 程序时直接输入相应的key\value

最后记得,secret_token一定要长~~~要长~~~,我们也可以直接用命令rake secret来替我们生成。