Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3396 from bdurand/session_cache_store
Add ActionDispatch::Session::CacheStore as a generic way of storing sessions in a cache
- Loading branch information
Showing
6 changed files
with
243 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
actionpack/lib/action_dispatch/middleware/session/cache_store.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
require 'action_dispatch/middleware/session/abstract_store' | ||
require 'rack/session/memcache' | ||
|
||
module ActionDispatch | ||
module Session | ||
# Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful | ||
# if you don't store critical data in your sessions and you don't need them to live for extended periods | ||
# of time. | ||
class CacheStore < AbstractStore | ||
# Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is | ||
# not specified, <tt>Rails.cache</tt> will be used. | ||
def initialize(app, options = {}) | ||
@cache = options[:cache] || Rails.cache | ||
options[:expire_after] ||= @cache.options[:expires_in] | ||
super | ||
end | ||
|
||
# Get a session from the cache. | ||
def get_session(env, sid) | ||
sid ||= generate_sid | ||
session = @cache.read(cache_key(sid)) | ||
session ||= {} | ||
[sid, session] | ||
end | ||
|
||
# Set a session in the cache. | ||
def set_session(env, sid, session, options) | ||
key = cache_key(sid) | ||
if session | ||
@cache.write(key, session, :expires_in => options[:expire_after]) | ||
else | ||
@cache.delete(key) | ||
end | ||
sid | ||
end | ||
|
||
# Remove a session from the cache. | ||
def destroy_session(env, sid, options) | ||
@cache.delete(cache_key(sid)) | ||
generate_sid | ||
end | ||
|
||
private | ||
# Turn the session id into a cache key. | ||
def cache_key(sid) | ||
"_session_id:#{sid}" | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
require 'abstract_unit' | ||
|
||
class CacheStoreTest < ActionDispatch::IntegrationTest | ||
class TestController < ActionController::Base | ||
def no_session_access | ||
head :ok | ||
end | ||
|
||
def set_session_value | ||
session[:foo] = "bar" | ||
head :ok | ||
end | ||
|
||
def set_serialized_session_value | ||
session[:foo] = SessionAutoloadTest::Foo.new | ||
head :ok | ||
end | ||
|
||
def get_session_value | ||
render :text => "foo: #{session[:foo].inspect}" | ||
end | ||
|
||
def get_session_id | ||
render :text => "#{request.session_options[:id]}" | ||
end | ||
|
||
def call_reset_session | ||
session[:bar] | ||
reset_session | ||
session[:bar] = "baz" | ||
head :ok | ||
end | ||
|
||
def rescue_action(e) raise end | ||
end | ||
|
||
def test_setting_and_getting_session_value | ||
with_test_route_set do | ||
get '/set_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
|
||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: "bar"', response.body | ||
end | ||
end | ||
|
||
def test_getting_nil_session_value | ||
with_test_route_set do | ||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: nil', response.body | ||
end | ||
end | ||
|
||
def test_getting_session_value_after_session_reset | ||
with_test_route_set do | ||
get '/set_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
session_cookie = cookies.send(:hash_for)['_session_id'] | ||
|
||
get '/call_reset_session' | ||
assert_response :success | ||
assert_not_equal [], headers['Set-Cookie'] | ||
|
||
cookies << session_cookie # replace our new session_id with our old, pre-reset session_id | ||
|
||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from cache" | ||
end | ||
end | ||
|
||
def test_getting_from_nonexistent_session | ||
with_test_route_set do | ||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: nil', response.body | ||
assert_nil cookies['_session_id'], "should only create session on write, not read" | ||
end | ||
end | ||
|
||
def test_setting_session_value_after_session_reset | ||
with_test_route_set do | ||
get '/set_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
session_id = cookies['_session_id'] | ||
|
||
get '/call_reset_session' | ||
assert_response :success | ||
assert_not_equal [], headers['Set-Cookie'] | ||
|
||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: nil', response.body | ||
|
||
get '/get_session_id' | ||
assert_response :success | ||
assert_not_equal session_id, response.body | ||
end | ||
end | ||
|
||
def test_getting_session_id | ||
with_test_route_set do | ||
get '/set_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
session_id = cookies['_session_id'] | ||
|
||
get '/get_session_id' | ||
assert_response :success | ||
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" | ||
end | ||
end | ||
|
||
def test_deserializes_unloaded_class | ||
with_test_route_set do | ||
with_autoload_path "session_autoload_test" do | ||
get '/set_serialized_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
end | ||
with_autoload_path "session_autoload_test" do | ||
get '/get_session_id' | ||
assert_response :success | ||
end | ||
with_autoload_path "session_autoload_test" do | ||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" | ||
end | ||
end | ||
end | ||
|
||
def test_doesnt_write_session_cookie_if_session_id_is_already_exists | ||
with_test_route_set do | ||
get '/set_session_value' | ||
assert_response :success | ||
assert cookies['_session_id'] | ||
|
||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" | ||
end | ||
end | ||
|
||
def test_prevents_session_fixation | ||
with_test_route_set do | ||
get '/get_session_value' | ||
assert_response :success | ||
assert_equal 'foo: nil', response.body | ||
session_id = cookies['_session_id'] | ||
|
||
reset! | ||
|
||
get '/set_session_value', :_session_id => session_id | ||
assert_response :success | ||
assert_not_equal session_id, cookies['_session_id'] | ||
end | ||
end | ||
|
||
private | ||
def with_test_route_set | ||
with_routing do |set| | ||
set.draw do | ||
match ':action', :to => ::CacheStoreTest::TestController | ||
end | ||
|
||
@app = self.class.build_app(set) do |middleware| | ||
cache = ActiveSupport::Cache::MemoryStore.new | ||
middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache | ||
middleware.delete "ActionDispatch::ShowExceptions" | ||
end | ||
|
||
yield | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters