Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/session/cookie_store.rb

Revision 6184, 3.9 kB (checked in by bitsweat, 4 years ago)

Introduce a cookie-based session store as the Rails default. Sessions typically contain at most a user_id and flash message; both fit within the 4K cookie size limit. A secure hash is included with the cookie to ensure data integrity (a user cannot alter his user_id without knowing the secret key included in the hash). If you have more than 4K of session data or don't want your data to be visible to the user, pick another session store. Cookie-based sessions are dramatically faster than the alternatives.

Line 
1 require 'cgi'
2 require 'cgi/session'
3 require 'base64'        # to make marshaled data HTTP header friendly
4 require 'digest/sha2'   # to generate the data integrity hash
5
6 # This cookie-based session store is the Rails default. Sessions typically
7 # contain at most a user_id and flash message; both fit within the 4K cookie
8 # size limit. Cookie-based sessions are dramatically faster than the
9 # alternatives.
10 #
11 # A secure hash is included with the cookie to ensure data integrity:
12 # a user cannot alter his user_id without knowing the secret key included in
13 # the hash. New apps are generated with a session :secret option in
14 # Application Controller. Set your own for old apps you're upgrading.
15 # Note that changing the secret invalidates all existing sessions!
16 #
17 # If you have more than 4K of session data or don't want your data to be
18 # visible to the user, pick another session store.
19 #
20 # CookieOverflow is raised if you attempt to store more than 4K of data.
21 # TamperedWithCookie is raised if the data integrity check fails.
22 class CGI::Session::CookieStore
23   # Cookies can typically store 4096 bytes.
24   MAX = 4096
25
26   # Raised when storing more than 4K of session data.
27   class CookieOverflow < StandardError; end
28
29   # Raised when the cookie fails its integrity check.
30   class TamperedWithCookie < StandardError; end
31
32   # Called from CGI::Session only.
33   def initialize(session, options = {})
34     # The secret option is required.
35     if options['secret'].blank?
36       raise ArgumentError, 'A secret is required to generate an integrity hash for cookie session data. Use session :secret => "some secret phrase" in ApplicationController.'
37     end
38
39     # Keep the session and its secret on hand so we can read and write cookies.
40     @session, @secret = session, options['secret']
41
42     # Default cookie options derived from session settings.
43     @cookie_options = {
44       'name'    => options['session_key'],
45       'path'    => options['session_path'],
46       'domain'  => options['session_domain'],
47       'expires' => options['session_expires'],
48       'secure'  => options['session_secure']
49     }
50
51     # Set no_hidden and no_cookies since the session id is unused and we
52     # set our own data cookie.
53     options['no_hidden'] = true
54     options['no_cookies'] = true
55   end
56
57   # Restore session data from the cookie.
58   def restore
59     @original = read_cookie
60     @data = unmarshal(@original) || {}
61   end
62
63   # Wait until close to write the session data cookie.
64   def update; end
65
66   # Write the session data cookie if it was loaded and has changed.
67   def close
68     if defined? @data
69       updated = marshal(@data)
70       raise CookieOverflow if updated.size > MAX
71       write_cookie('value' => updated) unless updated == @original
72     end
73   end
74
75   # Delete the session data by setting an expired cookie with no data.
76   def delete
77     write_cookie('value' => '', 'expires' => 1.year.ago)
78   end
79
80   private
81     # Marshal a session hash into safe cookie data. Include an integrity hash.
82     def marshal(session)
83       data = Base64.encode64(Marshal.dump(session)).chop
84       "#{data}--#{generate_digest(data)}"
85     end
86
87     # Unmarshal cookie data to a hash and verify its integrity.
88     def unmarshal(cookie)
89       if cookie
90         data, digest = cookie.split('--')
91         raise TamperedWithCookie unless digest == generate_digest(data)
92         Marshal.load(Base64.decode64(data))
93       end
94     end
95
96     # Generate the inline SHA512 message digest. Larger (128 bytes) than SHA256
97     # (64 bytes) or RMD160 (40 bytes), but small relative to the 4096 byte
98     # max cookie size.
99     def generate_digest(data)
100       Digest::SHA512.hexdigest "#{data}#{@secret}"
101     end
102
103     # Read the session data cookie.
104     def read_cookie
105       @session.cgi.cookies[@cookie_options['name']].first
106     end
107
108     # CGI likes to make you hack.
109     def write_cookie(options)
110       cookie = CGI::Cookie.new(@cookie_options.merge(options))
111       @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
112     end
113 end
Note: See TracBrowser for help on using the browser.