· 8 years ago · Jul 30, 2018, 02:56 PM
1From 5e6af94bd5d9d521439e619ec65239b53cd6568f Mon Sep 17 00:00:00 2001
2From: Brian Durand <brian@embellishedvisions.com>
3Date: Tue, 5 Apr 2011 14:00:32 -0500
4Subject: [PATCH] Add ActionDispatch::Session::CacheStore as a generic way of storing sessions in a cache. [#6675 state:resolved]
5
6---
7 actionpack/lib/action_dispatch.rb | 1 +
8 .../middleware/session/cache_store.rb | 50 ++++++
9 .../test/dispatch/session/cache_store_test.rb | 181 ++++++++++++++++++++
10 activesupport/lib/active_support/cache.rb | 11 +-
11 .../source/action_controller_overview.textile | 12 +-
12 railties/guides/source/rails_on_rack.textile | 5 +-
13 railties/guides/source/security.textile | 4 +-
14 7 files changed, 253 insertions(+), 11 deletions(-)
15 create mode 100644 actionpack/lib/action_dispatch/middleware/session/cache_store.rb
16 create mode 100644 actionpack/test/dispatch/session/cache_store_test.rb
17
18diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
19index 49971fc..7500b32 100644
20--- a/actionpack/lib/action_dispatch.rb
21+++ b/actionpack/lib/action_dispatch.rb
22@@ -81,6 +81,7 @@ module ActionDispatch
23 autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
24 autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
25 autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
26+ autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
27 end
28
29 autoload_under 'testing' do
30diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
31new file mode 100644
32index 0000000..d3b6fd1
33--- /dev/null
34+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
35@@ -0,0 +1,50 @@
36+require 'action_dispatch/middleware/session/abstract_store'
37+require 'rack/session/memcache'
38+
39+module ActionDispatch
40+ module Session
41+ # Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
42+ # if you don't store critical data in your sessions and you don't need them to live for extended periods
43+ # of time.
44+ class CacheStore < AbstractStore
45+ # Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is
46+ # not specified, <tt>Rails.cache</tt> will be used.
47+ def initialize(app, options = {})
48+ @cache = options[:cache] || Rails.cache
49+ options[:expire_after] ||= @cache.options[:expires_in]
50+ super
51+ end
52+
53+ # Get a session from the cache.
54+ def get_session(env, sid)
55+ sid ||= generate_sid
56+ session = @cache.read(cache_key(sid))
57+ session ||= {}
58+ [sid, session]
59+ end
60+
61+ # Set a session in the cache.
62+ def set_session(env, sid, session, options)
63+ key = cache_key(sid)
64+ if session
65+ @cache.write(key, session, :expires_in => options[:expire_after])
66+ else
67+ @cache.delete(key)
68+ end
69+ sid
70+ end
71+
72+ # Remove a session from the cache.
73+ def destroy_session(env, sid, options)
74+ @cache.delete(cache_key(sid))
75+ generate_sid
76+ end
77+
78+ private
79+ # Turn the session id into a cache key.
80+ def cache_key(sid)
81+ "_session_id:#{sid}"
82+ end
83+ end
84+ end
85+end
86diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
87new file mode 100644
88index 0000000..73e056d
89--- /dev/null
90+++ b/actionpack/test/dispatch/session/cache_store_test.rb
91@@ -0,0 +1,181 @@
92+require 'abstract_unit'
93+
94+class CacheStoreTest < ActionDispatch::IntegrationTest
95+ class TestController < ActionController::Base
96+ def no_session_access
97+ head :ok
98+ end
99+
100+ def set_session_value
101+ session[:foo] = "bar"
102+ head :ok
103+ end
104+
105+ def set_serialized_session_value
106+ session[:foo] = SessionAutoloadTest::Foo.new
107+ head :ok
108+ end
109+
110+ def get_session_value
111+ render :text => "foo: #{session[:foo].inspect}"
112+ end
113+
114+ def get_session_id
115+ render :text => "#{request.session_options[:id]}"
116+ end
117+
118+ def call_reset_session
119+ session[:bar]
120+ reset_session
121+ session[:bar] = "baz"
122+ head :ok
123+ end
124+
125+ def rescue_action(e) raise end
126+ end
127+
128+ def test_setting_and_getting_session_value
129+ with_test_route_set do
130+ get '/set_session_value'
131+ assert_response :success
132+ assert cookies['_session_id']
133+
134+ get '/get_session_value'
135+ assert_response :success
136+ assert_equal 'foo: "bar"', response.body
137+ end
138+ end
139+
140+ def test_getting_nil_session_value
141+ with_test_route_set do
142+ get '/get_session_value'
143+ assert_response :success
144+ assert_equal 'foo: nil', response.body
145+ end
146+ end
147+
148+ def test_getting_session_value_after_session_reset
149+ with_test_route_set do
150+ get '/set_session_value'
151+ assert_response :success
152+ assert cookies['_session_id']
153+ session_cookie = cookies.send(:hash_for)['_session_id']
154+
155+ get '/call_reset_session'
156+ assert_response :success
157+ assert_not_equal [], headers['Set-Cookie']
158+
159+ cookies << session_cookie # replace our new session_id with our old, pre-reset session_id
160+
161+ get '/get_session_value'
162+ assert_response :success
163+ assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from cache"
164+ end
165+ end
166+
167+ def test_getting_from_nonexistent_session
168+ with_test_route_set do
169+ get '/get_session_value'
170+ assert_response :success
171+ assert_equal 'foo: nil', response.body
172+ assert_nil cookies['_session_id'], "should only create session on write, not read"
173+ end
174+ end
175+
176+ def test_setting_session_value_after_session_reset
177+ with_test_route_set do
178+ get '/set_session_value'
179+ assert_response :success
180+ assert cookies['_session_id']
181+ session_id = cookies['_session_id']
182+
183+ get '/call_reset_session'
184+ assert_response :success
185+ assert_not_equal [], headers['Set-Cookie']
186+
187+ get '/get_session_value'
188+ assert_response :success
189+ assert_equal 'foo: nil', response.body
190+
191+ get '/get_session_id'
192+ assert_response :success
193+ assert_not_equal session_id, response.body
194+ end
195+ end
196+
197+ def test_getting_session_id
198+ with_test_route_set do
199+ get '/set_session_value'
200+ assert_response :success
201+ assert cookies['_session_id']
202+ session_id = cookies['_session_id']
203+
204+ get '/get_session_id'
205+ assert_response :success
206+ assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
207+ end
208+ end
209+
210+ def test_deserializes_unloaded_class
211+ with_test_route_set do
212+ with_autoload_path "session_autoload_test" do
213+ get '/set_serialized_session_value'
214+ assert_response :success
215+ assert cookies['_session_id']
216+ end
217+ with_autoload_path "session_autoload_test" do
218+ get '/get_session_id'
219+ assert_response :success
220+ end
221+ with_autoload_path "session_autoload_test" do
222+ get '/get_session_value'
223+ assert_response :success
224+ assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
225+ end
226+ end
227+ end
228+
229+ def test_doesnt_write_session_cookie_if_session_id_is_already_exists
230+ with_test_route_set do
231+ get '/set_session_value'
232+ assert_response :success
233+ assert cookies['_session_id']
234+
235+ get '/get_session_value'
236+ assert_response :success
237+ assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
238+ end
239+ end
240+
241+ def test_prevents_session_fixation
242+ with_test_route_set do
243+ get '/get_session_value'
244+ assert_response :success
245+ assert_equal 'foo: nil', response.body
246+ session_id = cookies['_session_id']
247+
248+ reset!
249+
250+ get '/set_session_value', :_session_id => session_id
251+ assert_response :success
252+ assert_not_equal session_id, cookies['_session_id']
253+ end
254+ end
255+
256+ private
257+ def with_test_route_set
258+ with_routing do |set|
259+ set.draw do
260+ match ':action', :to => ::CacheStoreTest::TestController
261+ end
262+
263+ @app = self.class.build_app(set) do |middleware|
264+ cache = ActiveSupport::Cache::MemoryStore.new
265+ middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache
266+ middleware.delete "ActionDispatch::ShowExceptions"
267+ end
268+
269+ yield
270+ end
271+ end
272+end
273diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
274index 10c457b..fc0e5b3 100644
275--- a/activesupport/lib/active_support/cache.rb
276+++ b/activesupport/lib/active_support/cache.rb
277@@ -608,14 +608,21 @@ module ActiveSupport
278 end
279
280 # Returns the size of the cached value. This could be less than value.size
281- # if the data is compressed.
282+ # if the data is compressed. This value is used for calculating approximate
283+ # cache sizes and should not be considered authoritative.
284 def size
285 if @value.nil?
286 0
287 elsif @value.respond_to?(:bytesize)
288 @value.bytesize
289 else
290- Marshal.dump(@value).bytesize
291+ begin
292+ Marshal.dump(@value).bytesize
293+ rescue
294+ # Just guess at the size if there is a problem with marshalling.
295+ # If there is a real error, it will be raise when the object is used.
296+ @value.inspect.bytesize
297+ end
298 end
299 end
300
301diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile
302index 496dc72..c9e791f 100644
303--- a/railties/guides/source/action_controller_overview.textile
304+++ b/railties/guides/source/action_controller_overview.textile
305@@ -140,17 +140,19 @@ h3. Session
306
307 Your application has a session for each user in which you can store small amounts of data that will be persisted between requests. The session is only available in the controller and the view and can use one of a number of different storage mechanisms:
308
309-* CookieStore - Stores everything on the client.
310-* DRbStore - Stores the data on a DRb server.
311-* MemCacheStore - Stores the data in a memcache.
312-* ActiveRecordStore - Stores the data in a database using Active Record.
313+* ActionDispatch::Session::CookieStore - Stores everything on the client.
314+* ActiveRecord::SessionStore - Stores the data in a database using Active Record.
315+* ActionDispatch::Session::CacheStore - Stores the data in the Rails cache.
316+* ActionDispatch::Session::MemCacheStore - Stores the data in a memcached cluster (this is a legacy implementation; consider using CacheStore instead).
317
318 All session stores use a cookie to store a unique ID for each session (you must use a cookie, Rails will not allow you to pass the session ID in the URL as this is less secure).
319
320-For most stores this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof, but it is not encrypted, so anyone with access to it can read its contents but not edit it (Rails will not accept it if it has been edited).
321+For most stores this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof, but it is not encrypted, so anyone with access to it can read its contents but not edit it (Rails will not accept it if it has been edited).
322
323 The CookieStore can store around 4kB of data -- much less than the others -- but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.
324
325+If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using ActionDispatch::Session::CacheStore. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time.
326+
327 Read more about session storage in the "Security Guide":security.html.
328
329 If you need a different session storage mechanism, you can change it in the +config/initializers/session_store.rb+ file:
330diff --git a/railties/guides/source/rails_on_rack.textile b/railties/guides/source/rails_on_rack.textile
331index b1db294..7d37ad3 100644
332--- a/railties/guides/source/rails_on_rack.textile
333+++ b/railties/guides/source/rails_on_rack.textile
334@@ -161,8 +161,9 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
335 |+Rack::Lock+|Sets +env["rack.multithread"]+ flag to +true+ and wraps the application within a Mutex.|
336 |+ActionController::Failsafe+|Returns HTTP Status +500+ to the client if an exception gets raised while dispatching.|
337 |+ActiveRecord::QueryCache+|Enables the Active Record query cache.|
338-|+ActionController::Session::CookieStore+|Uses the cookie based session store.|
339-|+ActionController::Session::MemCacheStore+|Uses the memcached based session store.|
340+|+ActionDispatch::Session::CookieStore+|Uses the cookie based session store.|
341+|+ActionDispatch::Session::CacheStore+|Uses the Rails cache based session store.|
342+|+ActionDispatch::Session::MemCacheStore+|Uses the memcached based session store.|
343 |+ActiveRecord::SessionStore+|Uses the database based session store.|
344 |+Rack::MethodOverride+|Sets HTTP method based on +_method+ parameter or +env["HTTP_X_HTTP_METHOD_OVERRIDE"]+.|
345 |+Rack::Head+|Discards the response body if the client sends a +HEAD+ request.|
346diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile
347index 893f658..5fc1770 100644
348--- a/railties/guides/source/security.textile
349+++ b/railties/guides/source/security.textile
350@@ -83,9 +83,9 @@ This will also be a good idea, if you modify the structure of an object and old
351
352 h4. Session Storage
353
354--- _Rails provides several storage mechanisms for the session hashes. The most important are ActiveRecordStore and CookieStore._
355+-- _Rails provides several storage mechanisms for the session hashes. The most important are ActiveRecord::SessionStore and ActionDispatch::Session::CookieStore._
356
357-There are a number of session storages, i.e. where Rails saves the session hash and session id. Most real-live applications choose ActiveRecordStore (or one of its derivatives) over file storage due to performance and maintenance reasons. ActiveRecordStore keeps the session id and hash in a database table and saves and retrieves the hash on every request.
358+There are a number of session storages, i.e. where Rails saves the session hash and session id. Most real-live applications choose ActiveRecord::SessionStore (or one of its derivatives) over file storage due to performance and maintenance reasons. ActiveRecord::SessionStore keeps the session id and hash in a database table and saves and retrieves the hash on every request.
359
360 Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it:
361
362--
3631.7.3.4