Table of Contents
如果想在已有的 Rails app 上使用其它语言加些 API,同时能直接使用 Rails 的登陆信息,最简单的就是用 Nginx 等代理将不同的服务映射到相同的域名下,其它的 App 解密 Cookie 获得登陆信息。
本文以 Ruby 代码为例说明 Rails 的 Cookie 是如何加密,然后以 Go 为例说明如何解密的。

Rails 的实现可以参考 ActiveSupport::MessageEncryptor,ActiveSupport::MessageVerifier 和相应的单元测试。
加密
上图说明了原始的 Session 对象 Session Data 是如何最终生成 Cookie 的。如果登陆用了 Devise,那么 Session Data 中的登陆信息保存在 warden.user.user.key
中。之后就用下面例子说明加密。
session = { "warden.user.user.key" => [[1],"secret"] }
① Cookie Serializer
从 Rails 4.1 开始,默认使用的 JSON,4.1 之前使用的 Ruby Marshal。为了方便其它语言中解析,推荐使用 4.1 或更新的版本并使用 JSON 做为 Cookie 的 serializer。配置在 config/initializers/cookies_serializer.rb
中
Rails.application.config.action_dispatch.cookies_serializer = :json
JSON 的 serializer 就很直接了
require 'json'
session_json = JSON.dump(session)
puts session_json.inspect
# => "{\"warden.user.user.key\":[[1],\"secret\"]}"
② Padding
下一步的加密要求数据的字节数必须是 16 的倍数,用的算法是 PKCS7。简单说就是如果差 n 个字节到下个 16 的倍数就补 n 个 n。如果刚好是 16 的倍数就补 16 个 16。
def padding(data, block_size = 16)
n = block_size - data.bytesize % block_size
return data.force_encoding('ASCII-8BIT') + n.chr * n
end
padded_session = padding(session_json)
puts padded_session.inspect
# => "{\"warden.user.user.key\":[[1],\"secret\"]}\t\t\t\t\t\t\t\t\t"
末尾的 \t
ASCII 码是 9,表示补了 9 个字节。
③ 加密 AES-CBC
这一步是最主要的加密了,算法是 AES-CBC。加密需要配置密钥并随机生成 IV (initialization vector)。因为 Ruby 的 OpenSSL::Cipher 封装会自动 padding,所以可以跳过第 ② 步。
我们知道 Rails 需要配置 secret key base,密钥就是通过 secret key base 和 salt 产生的,使用的算法 pbkdf2
在 OpenSSL 里也提供了。
OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, keylen)
pass
配置中的 secret key basesalt
如果使用默认 Rails 配置的话,加密是encrypted cookie
,后面签名步骤是signed encrypted cookie
iter
默认是 1000,keylen
加密是 32,签名是 64。也可以统一用 64,但是加密的 Key 只取前 32 个字节。
require 'openssl'
SECRET_KEY_BASE = "development_secret"
DEFAULT_SALT = "encrypted cookie"
DEFAULT_SIGN_SALT = "signed encrypted cookie"
DEFAULT_ITER = 1000
DEFAULT_KEYLEN = 64
def generate_key(secret_key_base, salt, iter = DEFAULT_ITER, keylen = DEFAULT_KEYLEN)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, salt, iter, keylen)
end
encrypt_key = generate_key(SECRET_KEY_BASE, DEFAULT_SALT)[0...32]
puts Base64.strict_encode64(encrypt_key)
# => vozBHj31liL/p88es/k7aywa4Po4mwMVkW/eqhFjw/4=
IV 是随机的 16 个字节。解密的时候需要用到,所以需要保存起来下一步拼装的时候用。可以用 SecureRandom.random_bytes
或者 OpenSSL::Cipher.random_iv
。
使用 OpenSSL 实现如下,IV 应该要随机的,为了方便对照,直接用了 16 个 0
encrypt_key = generate_key(SECRET_KEY_BASE, DEFAULT_SALT)[0...32]
puts Base64.strict_encode64(encrypt_key)
cipher = OpenSSL::Cipher.new("aes-256-cbc")
cipher.encrypt
cipher.key = encrypt_key
iv = "\0" * 16
# iv = cipher.random_iv
encrypted_content = cipher.update(session_json)
encrypted_content << cipher.final
puts Base64.strict_encode64(encrypted_content)
# => t7c1ncaCXhZAOPRtX0BI8eceOmx/Qg3Jrg6uwmgJuSNosKIc7M4KRfOw1q3mFWv7ZSiNO3ZRPxJMGI1cDvu+PQ==
④ 拼装加密内容和 IV
得到 encrypted_content
和 iv
后,分别 base64 后用 --
连接,然后再做一次 base64 得到 encrypted_data
encrypted_data = Base64.strict_encode64(
Base64.strict_encode64(encrypted_content) +
"--" +
Base64.strict_encode64(iv)
)
puts encrypted_data
# => dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09
⑤ 签名 HMAC-SHA1
签名用的 HMAC-SHA1
,结果转成 16 进制字符串。Key 参考 加密步骤中的说明。
sign_key = generate_key(SECRET_KEY_BASE, DEFAULT_SIGN_SALT)
sign = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_key, encrypted_data)
puts sign
# => 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
⑥ 拼装签名
最后把 encrypted_data
和 sign
用 --
连接然后做一次 URL Query Escape 就可以了
require "uri"
cookie_content = URI.encode_www_form_component(encrypted_data + "--" + sign)
puts cookie_content
# => dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
完整的代码: rails-cookie-encrypt.rb
如果用 ActiveSupport
可以简化成
require "active_support/key_generator"
require "active_support/message_encryptor"
encrypt_key = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: DEFAULT_ITER).generate_key(DEFAULT_SALT, 32)
sign_key = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: DEFAULT_ITER).generate_key(DEFAULT_SIGN_SALT, 64)
encryptor = ActiveSupport::MessageEncryptor.new(encrypt_key, sign_key, serializer: JSON)
puts encryptor.encrypt_and_sign(session)
解密
解密就是把 6 个步骤反过来,输入就是
cookieContent := "dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6"
⑥ 分离签名
URL Query Unescape 然后以 --
分成 encryptedData
和 sign
var err error
var unescapedCookieContent string
if unescapedCookieContent, err = url.QueryUnescape(cookieContent); err != nil {
panic(err)
}
encryptedDataSignVectors := strings.SplitN(unescapedCookieContent, "--", 2)
encryptedData := encryptedDataSignVectors[0]
sign := encryptedDataSignVectors[1]
fmt.Printf("encrypted_data = %v\n", encryp