<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Golang</title><link>https://blog.iany.me/zh/tags/golang/</link><description>Golang的最新内容 «~iany/»</description><language>zh-CN</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Sat, 23 Sep 2017 07:47:49 +0000</lastBuildDate><atom:link href="https://blog.iany.me/zh/tags/golang/index.xml" rel="self" type="application/rss+xml"/><item><title>Rails Cookie 如何解密</title><link>https://blog.iany.me/zh/2017/09/rails-cookie-encryption/</link><pubDate>Sat, 23 Sep 2017 07:47:49 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/09/rails-cookie-encryption/</guid><description>&lt;p&gt;如果想在已有的 Rails app 上使用其它语言加些 API，同时能直接使用 Rails 的登陆信息，最简单的就是用 Nginx 等代理将不同的服务映射到相同的域名下，其它的 App 解密 Cookie 获得登陆信息。&lt;/p&gt;
&lt;p&gt;本文以 Ruby 代码为例说明 Rails 的 Cookie 是如何加密，然后以 Go 为例说明如何解密的。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Rails Cookie 加密" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_c566275d3ee4f967.png" srcset="https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_4af7debedc226724.png 400w, https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_c566275d3ee4f967.png 543w" sizes="(max-width: 400px) 100vw, 543px" /&gt;
&lt;figcaption &gt;Rails Cookie 加密&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Rails 的实现可以参考 &lt;a href="https://github.com/rails/rails/blob/0a6f69a5debf89748da3a43747c61d201095997e/activesupport/lib/active_support/message_encryptor.rb"&gt;ActiveSupport::MessageEncryptor&lt;/a&gt;，&lt;a href="https://github.com/rails/rails/blob/0a6f69a5debf89748da3a43747c61d201095997e/activesupport/lib/active_support/message_verifier.rb"&gt;ActiveSupport::MessageVerifier&lt;/a&gt; 和相应的单元测试。&lt;/p&gt;
&lt;h2 id="加密"&gt;加密&lt;/h2&gt;
&lt;p&gt;上图说明了原始的 Session 对象 &lt;em&gt;Session Data&lt;/em&gt; 是如何最终生成 Cookie 的。如果登陆用了 &lt;a href="https://github.com/plataformatec/devise"&gt;Devise&lt;/a&gt;，那么 Session Data 中的登陆信息保存在 &lt;code&gt;warden.user.user.key&lt;/code&gt; 中。之后就用下面例子说明加密。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;session = { &amp;quot;warden.user.user.key&amp;quot; =&amp;gt; [[1],&amp;quot;secret&amp;quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-cookie-serializer"&gt;① Cookie Serializer&lt;/h3&gt;
&lt;p&gt;从 Rails 4.1 开始，默认使用的 JSON，4.1 之前使用的 Ruby Marshal。为了方便其它语言中解析，推荐使用 4.1 或更新的版本并使用 JSON 做为 Cookie 的 serializer。配置在 &lt;code&gt;config/initializers/cookies_serializer.rb&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;Rails.application.config.action_dispatch.cookies_serializer = :json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON 的 serializer 就很直接了&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require 'json'
session_json = JSON.dump(session)
puts session_json.inspect
# =&amp;gt; &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-padding"&gt;② Padding&lt;/h3&gt;
&lt;p&gt;下一步的加密要求数据的字节数必须是 16 的倍数，用的算法是 &lt;a href="https://en.wikipedia.org/wiki/Padding_%28cryptography%29#PKCS7"&gt;PKCS7&lt;/a&gt;。简单说就是如果差 n 个字节到下个 16 的倍数就补 n 个 n。如果刚好是 16 的倍数就补 16 个 16。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;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
# =&amp;gt; &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}\t\t\t\t\t\t\t\t\t&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;末尾的 &lt;code&gt;\t&lt;/code&gt; ASCII 码是 9，表示补了 9 个字节。&lt;/p&gt;
&lt;h3 id="-加密-aes-cbc"&gt;③ 加密 AES-CBC&lt;/h3&gt;
&lt;p&gt;这一步是最主要的加密了，算法是 AES-CBC。加密需要配置密钥并随机生成 IV (initialization vector)。因为 Ruby 的 OpenSSL::Cipher 封装会自动 padding，所以可以跳过第 ② 步。&lt;/p&gt;
&lt;p&gt;我们知道 Rails 需要配置 secret key base，密钥就是通过 secret key base 和 salt 产生的，使用的算法 &lt;code&gt;pbkdf2&lt;/code&gt; 在 OpenSSL 里也提供了。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, keylen)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pass&lt;/code&gt; 配置中的 secret key base&lt;/li&gt;
&lt;li&gt;&lt;code&gt;salt&lt;/code&gt; 如果使用默认 Rails 配置的话，加密是 &lt;code&gt;encrypted cookie&lt;/code&gt;，后面签名步骤是 &lt;code&gt;signed encrypted cookie&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iter&lt;/code&gt; 默认是 1000, &lt;code&gt;keylen&lt;/code&gt; 加密是 32，签名是 64。也可以统一用 64，但是加密的 Key 只取前 32 个字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require 'openssl'
SECRET_KEY_BASE = &amp;quot;development_secret&amp;quot;
DEFAULT_SALT = &amp;quot;encrypted cookie&amp;quot;
DEFAULT_SIGN_SALT = &amp;quot;signed encrypted cookie&amp;quot;
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)
# =&amp;gt; vozBHj31liL/p88es/k7aywa4Po4mwMVkW/eqhFjw/4=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IV 是随机的 16 个字节。解密的时候需要用到，所以需要保存起来下一步拼装的时候用。可以用 &lt;code&gt;SecureRandom.random_bytes&lt;/code&gt; 或者 &lt;code&gt;OpenSSL::Cipher.random_iv&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用 OpenSSL 实现如下，IV 应该要随机的，为了方便对照，直接用了 16 个 0&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;encrypt_key = generate_key(SECRET_KEY_BASE, DEFAULT_SALT)[0...32]
puts Base64.strict_encode64(encrypt_key)
cipher = OpenSSL::Cipher.new(&amp;quot;aes-256-cbc&amp;quot;)
cipher.encrypt
cipher.key = encrypt_key
iv = &amp;quot;\0&amp;quot; * 16
# iv = cipher.random_iv
encrypted_content = cipher.update(session_json)
encrypted_content &amp;lt;&amp;lt; cipher.final
puts Base64.strict_encode64(encrypted_content)
# =&amp;gt; t7c1ncaCXhZAOPRtX0BI8eceOmx/Qg3Jrg6uwmgJuSNosKIc7M4KRfOw1q3mFWv7ZSiNO3ZRPxJMGI1cDvu+PQ==
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-拼装加密内容和-iv"&gt;④ 拼装加密内容和 IV&lt;/h3&gt;
&lt;p&gt;得到 &lt;code&gt;encrypted_content &lt;/code&gt; 和 &lt;code&gt;iv&lt;/code&gt; 后，分别 base64 后用 &lt;code&gt;--&lt;/code&gt; 连接，然后再做一次 base64 得到 &lt;code&gt;encrypted_data&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;encrypted_data = Base64.strict_encode64(
Base64.strict_encode64(encrypted_content) +
&amp;quot;--&amp;quot; +
Base64.strict_encode64(iv)
)
puts encrypted_data
# =&amp;gt; dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-签名-hmac-sha1"&gt;⑤ 签名 HMAC-SHA1&lt;/h3&gt;
&lt;p&gt;签名用的 &lt;code&gt;HMAC-SHA1&lt;/code&gt;，结果转成 16 进制字符串。Key 参考 加密步骤中的说明。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;sign_key = generate_key(SECRET_KEY_BASE, DEFAULT_SIGN_SALT)
sign = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_key, encrypted_data)
puts sign
# =&amp;gt; 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-拼装签名"&gt;⑥ 拼装签名&lt;/h3&gt;
&lt;p&gt;最后把 &lt;code&gt;encrypted_data&lt;/code&gt; 和 &lt;code&gt;sign&lt;/code&gt; 用 &lt;code&gt;--&lt;/code&gt; 连接然后做一次 URL Query Escape 就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require &amp;quot;uri&amp;quot;
cookie_content = URI.encode_www_form_component(encrypted_data + &amp;quot;--&amp;quot; + sign)
puts cookie_content
# =&amp;gt; dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码: &lt;a href="https://gist.github.com/doitian/2a89dc9e4372e55335c9111f576b47bf#file-rails-cookie-encrypt-rb"&gt;rails-cookie-encrypt.rb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果用 &lt;code&gt;ActiveSupport&lt;/code&gt; 可以简化成&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require &amp;quot;active_support/key_generator&amp;quot;
require &amp;quot;active_support/message_encryptor&amp;quot;
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)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="解密"&gt;解密&lt;/h2&gt;
&lt;p&gt;解密就是把 6 个步骤反过来，输入就是&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;cookieContent := &amp;quot;dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-分离签名"&gt;⑥ 分离签名&lt;/h3&gt;
&lt;p&gt;URL Query Unescape 然后以 &lt;code&gt;--&lt;/code&gt; 分成 &lt;code&gt;encryptedData&lt;/code&gt; 和 &lt;code&gt;sign&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var err error
var unescapedCookieContent string
if unescapedCookieContent, err = url.QueryUnescape(cookieContent); err != nil {
panic(err)
}
encryptedDataSignVectors := strings.SplitN(unescapedCookieContent, &amp;quot;--&amp;quot;, 2)
encryptedData := encryptedDataSignVectors[0]
sign := encryptedDataSignVectors[1]
fmt.Printf(&amp;quot;encrypted_data = %v\n&amp;quot;, encryptedData)
fmt.Printf(&amp;quot;sign = %v\n&amp;quot;, sign)
// =&amp;gt; encrypted_data = dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09
// sign = 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-验证签名"&gt;⑤ 验证签名&lt;/h3&gt;
&lt;p&gt;验证签名其实就是再签一次然后对比结果。为了安全，可以使用 &lt;code&gt;hmac.Equal&lt;/code&gt; 来比较签名是否一致。&lt;/p&gt;
&lt;p&gt;Key 的生成可以使用 &lt;code&gt;golang.org/x/crypto/pbkdf2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;const (
keyIterNum = 1000
keySize = 64
)
func generateKey(base, salt string) []byte {
return pbkdf2.Key([]byte(base), []byte(salt), keyIterNum, keySize, sha1.New)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;secretKeyBase := &amp;quot;development_secret&amp;quot;
defaultSignSalt := &amp;quot;signed encrypted cookie&amp;quot;
signKey := generateKey(secretKeyBase, defaultSignSalt)
signHmac := hmac.New(sha1.New, signKey)
signHmac.Write([]byte(encryptedData))
verifySign := signHmac.Sum(nil)
fmt.Printf(&amp;quot;verifySign = %v\n&amp;quot;, hex.EncodeToString(verifySign))
// verifySign = 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
var signDecoded []byte
if signDecoded, err = hex.DecodeString(sign); err != nil {
panic(err)
}
if !hmac.Equal(verifySign, signDecoded) {
panic(fmt.Errorf(&amp;quot;verification failed&amp;quot;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-分离加密内容和-iv"&gt;④ 分离加密内容和 IV&lt;/h3&gt;
&lt;p&gt;Base64 解码一次，用 &lt;code&gt;--&lt;/code&gt; 分离并分别 Base64 解码得到 &lt;code&gt;encryptedContent&lt;/code&gt; 和 &lt;code&gt;iv&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var encryptedDataBase64Decoded []byte
if encryptedDataBase64Decoded, err = base64.StdEncoding.DecodeString(encryptedData); err != nil {
panic(err)
}
encryptedContentIvVectors := strings.SplitN(string(encryptedDataBase64Decoded), &amp;quot;--&amp;quot;, 2)
var encryptedContent []byte
var iv []byte
if encryptedContent, err = base64.StdEncoding.DecodeString(encryptedContentIvVectors[0]); err != nil {
panic(err)
}
if iv, err = base64.StdEncoding.DecodeString(encryptedContentIvVectors[1]); err != nil {
panic(err)
}
fmt.Printf(&amp;quot;encrypted_content = %s\n&amp;quot;, base64.StdEncoding.EncodeToString(encryptedContent))
fmt.Printf(&amp;quot;iv = %v\n&amp;quot;, iv)
// encrypted_content = t7c1ncaCXhZAOPRtX0BI8eceOmx/Qg3Jrg6uwmgJuSNosKIc7M4KRfOw1q3mFWv7
// iv = [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-解密"&gt;③ 解密&lt;/h3&gt;
&lt;p&gt;用 Key 和 iv 来解密&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;defaultSalt := &amp;quot;encrypted cookie&amp;quot;
encryptKey := generateKey(secretKeyBase, defaultSalt)[:32]
c, err := aes.NewCipher(encryptKey)
if err != nil {
panic(err)
}
cfb := cipher.NewCBCDecrypter(c, iv)
paddedSession := make([]byte, len(encryptedContent))
cfb.CryptBlocks(paddedSession, encryptedContent)
fmt.Printf(&amp;quot;padded_session = %s\n&amp;quot;, strconv.QuoteToASCII(string(paddedSession)))
// padded_session = &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}\t\t\t\t\t\t\t\t\t&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-un-padding"&gt;② Un-padding&lt;/h3&gt;
&lt;p&gt;去除 padding 只需要看最后一个字节是多少就移除多少个字节。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;padding := int(paddedSession[len(paddedSession) - 1])
sessionJSON := string(paddedSession[:(len(paddedSession) - padding)])
fmt.Printf(&amp;quot;session_json = %s\n&amp;quot;, sessionJSON)
// session_json = {&amp;quot;warden.user.user.key&amp;quot;:[[1],&amp;quot;secret&amp;quot;]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-cookie-deserializer"&gt;① Cookie Deserializer&lt;/h3&gt;
&lt;p&gt;如果是 JSON 用 go JSON 库解析就可以了。如果是 Ruby Marshal 也不用完整实现，可以用正则提取需要的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var jsonData map[string]interface{}
if err := json.Unmarshal([]byte(sessionJSON), &amp;amp;jsonData); err != nil {
panic(err)
}
fmt.Printf(&amp;quot;%+v\n&amp;quot;, jsonData)
// map[warden.user.user.key:[[1] secret]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码: &lt;a href="https://gist.github.com/doitian/2a89dc9e4372e55335c9111f576b47bf#file-rails-cookie-decrypt-go"&gt;rails-cookie-decrypt.go&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果 Rails 里用的 Devise，可以在 &lt;code&gt;config/initializers/devise.rb&lt;/code&gt; 增加下面的配置来在 Cookie 中包含更多的字段，比如用户名或邮箱&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;Warden::Manager.after_authentication do |user, auth, opts|
auth.raw_session['warden.user.user.email'] = user.email
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要用户重新登陆或者更换 secret key base 才会生效。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/">~iany/</category><category domain="https://blog.iany.me/zh/tags/encryption/">Encryption</category><category domain="https://blog.iany.me/zh/tags/golang/">Golang</category><category domain="https://blog.iany.me/zh/tags/rails/">Rails</category><category domain="https://blog.iany.me/zh/tags/ruby/">Ruby</category></item></channel></rss>