From f9aa89afe6f31de35e5ec1541526290e8dac6185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=AA=E5=A9=A7=E6=B3=A0?= Date: Tue, 30 Aug 2016 15:25:50 +0800 Subject: [PATCH] added: adapter for java spring mvc (still lack ut for JavaSpringAdapter) --- .gitignore | 1 + lib/redis-session-store.rb | 142 +++++++++++++++++++++++-------- spec/redis_session_store_spec.rb | 37 ++++---- 3 files changed, 131 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index d87d4be..d9ee225 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.gem *.rbc +*.swp .bundle .config .yardoc diff --git a/lib/redis-session-store.rb b/lib/redis-session-store.rb index a6805d2..bcddbd4 100644 --- a/lib/redis-session-store.rb +++ b/lib/redis-session-store.rb @@ -1,4 +1,5 @@ require 'redis' +require 'securerandom' # Redis session storage for Rails, and for Rails only. Derived from # the MemCacheStore code, simply dropping in Redis instead. @@ -18,11 +19,13 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore # * +:redis+ - A hash with redis-specific options # * +:url+ - Redis url, default is redis://localhost:6379/0 # * +:key_prefix+ - Prefix for keys used in Redis, e.g. +myapp:+ + # * +:hashkey_prefix+ - Prefix for hashkeys if session saved as hash # * +:expire_after+ - A number in seconds for session timeout # * +:client+ - Connect to Redis with given object rather than create one # * +:on_redis_down:+ - Called with err, env, and SID on Errno::ECONNREFUSED # * +:on_session_load_error:+ - Called with err and SID on Marshal.load fail # * +:serializer:+ - Serializer to use on session data, default is :marshal. + # * +:adapter:+ - Adapter for other framework's session, default is :default. # # ==== Examples # @@ -47,6 +50,7 @@ def initialize(app, options = {}) @redis = redis_options[:client] || Redis.new(redis_options) @on_redis_down = options[:on_redis_down] @serializer = determine_serializer(options[:serializer]) + @adapter = determine_adapter(options[:adapter]) @on_session_load_error = options[:on_session_load_error] verify_handlers! end @@ -55,7 +59,7 @@ def initialize(app, options = {}) private - attr_reader :redis, :key, :default_options, :serializer + attr_reader :redis, :key, :default_options, :serializer, :adapter # overrides method defined in rack to actually verify session existence # Prevents needless new sessions from being created in scenario where @@ -86,52 +90,38 @@ def prefixed(sid) "#{default_options[:key_prefix]}#{sid}" end - def get_session(env, sid) - unless sid && (session = load_session_from_redis(sid)) - sid = generate_sid - session = {} - end - - [sid, session] - rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e - on_redis_down.call(e, env, sid) if on_redis_down - [generate_sid, {}] + def generate_session_sid + adapter.generate_session_sid || generate_sid end - alias find_session get_session - def load_session_from_redis(sid) - data = redis.get(prefixed(sid)) - begin - data ? decode(data) : nil - rescue => e - destroy_session_from_sid(sid, drop: true) - on_session_load_error.call(e, sid) if on_session_load_error - nil - end + def get_session(env, sid) + session = load_session_from_redis(env, sid) if sid + return [sid, session] if sid && session + [generate_session_sid, {}] end + alias find_session get_session - def decode(data) - serializer.load(data) + def load_session_from_redis(env, sid) + return adapter.load_session_from_redis(prefixed(sid)) + rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e + on_redis_down.call(e, env, sid) if on_redis_down + nil + rescue => e + destroy_session_from_sid(sid, drop: true) + on_session_load_error.call(e, sid) if on_session_load_error + nil end def set_session(env, sid, session_data, options = nil) expiry = (options || env.fetch(ENV_SESSION_OPTIONS_KEY))[:expire_after] - if expiry - redis.setex(prefixed(sid), expiry, encode(session_data)) - else - redis.set(prefixed(sid), encode(session_data)) - end + adapter.write_session_to_redis(prefixed(sid), session_data, expiry) return sid rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e on_redis_down.call(e, env, sid) if on_redis_down - return false + false end alias write_session set_session - def encode(session_data) - serializer.dump(session_data) - end - def destroy_session(env, sid, options) destroy_session_from_sid(sid, (options || {}).to_hash.merge(env: env)) end @@ -147,7 +137,7 @@ def destroy(env) def destroy_session_from_sid(sid, options = {}) redis.del(prefixed(sid)) - (options || {})[:drop] ? nil : generate_sid + (options || {})[:drop] ? nil : generate_session_sid rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down end @@ -189,4 +179,88 @@ def self.needs_migration?(value) value.start_with?(MARSHAL_SIGNATURE) end end + + def determine_adapter(adapter) + adapter ||= :default + case adapter + when :default then DefaultAdapter.new(redis, serializer) + when :java_spring then + JavaSpringAdapter.new(redis, serializer, default_options[:hashkey_prefix]) + else adapter + end + end + + # Default adapter, save session in redis as string + class DefaultAdapter + def initialize(redis, serializer) + @redis = redis + @serializer = serializer + end + + attr_accessor :redis, :serializer + + def generate_session_sid + end + + def load_session_from_redis(s_key) + data = redis.get(s_key) + data ? serializer.load(data) : nil + end + + def write_session_to_redis(s_key, session_data, expiry) + if expiry + redis.setex(s_key, expiry, serializer.dump(session_data)) + else + redis.set(s_key, serializer.dump(session_data)) + end + end + end + + # Java Spring adapter, save session in redis as hash + class JavaSpringAdapter < DefaultAdapter + def initialize(redis, serializer, hashkey_prefix) + @redis = redis + @serializer = serializer + @hashkey_prefix = hashkey_prefix || '' + end + + attr_accessor :redis, :serializer, :hashkey_prefix + + def generate_session_sid + SecureRandom.uuid + end + + def load_session_from_redis(s_key) + data = {} + redis.hkeys(s_key).each do |key| + next unless key.start_with?(hashkey_prefix) + value = redis.hget(s_key, key).to_s + key = key[hashkey_prefix.length..-1] + data[key] = serializer.load(value) unless value.empty? + end + data + end + + def write_session_to_redis(s_key, session_data, expiry) + keys = [] + session_data.each do |key, value| + key = "#{hashkey_prefix}#{key}" + unless value.nil? + redis.hset(s_key, key, serializer.dump(value)) + keys << key + end + end + clear_old_keys(s_key, keys) + redis.expire(s_key, expiry) if expiry + end + + private + + def clear_old_keys(s_key, new_keys) + redis.hkeys(s_key).each do |key| + next unless key.start_with?(hashkey_prefix) && !new_keys.include?(key) + redis.hdel(s_key, key) + end + end + end end diff --git a/spec/redis_session_store_spec.rb b/spec/redis_session_store_spec.rb index 94396b4..8acc782 100644 --- a/spec/redis_session_store_spec.rb +++ b/spec/redis_session_store_spec.rb @@ -148,7 +148,8 @@ context 'when unsuccessfully persisting the session' do before do - allow(store).to receive(:redis).and_raise(Redis::CannotConnectError) + allow(store.instance_variable_get(:@adapter)) + .to receive(:redis).and_raise(Redis::CannotConnectError) end it 'returns false' do @@ -168,7 +169,8 @@ context 'when redis is down' do before do - allow(store).to receive(:redis).and_raise(Redis::CannotConnectError) + allow(store.instance_variable_get(:@adapter)) + .to receive(:redis).and_raise(Redis::CannotConnectError) store.on_redis_down = ->(*_a) { @redis_down_handled = true } end @@ -260,7 +262,8 @@ it 'retrieves the prefixed key from redis' do redis = double('redis') - allow(store).to receive(:redis).and_return(redis) + allow(store.instance_variable_get(:@adapter)) + .to receive(:redis).and_return(redis) allow(store).to receive(:generate_sid).and_return(fake_key) expect(redis).to receive(:get).with("#{options[:key_prefix]}#{fake_key}") @@ -269,7 +272,8 @@ context 'when redis is down' do before do - allow(store).to receive(:redis).and_raise(Redis::CannotConnectError) + allow(store.instance_variable_get(:@adapter)) + .to receive(:redis).and_raise(Redis::CannotConnectError) allow(store).to receive(:generate_sid).and_return('foop') end @@ -357,7 +361,8 @@ let(:expected_encoding) { encoded_data } before do - allow(store).to receive(:redis).and_return(redis) + allow(store.instance_variable_get(:@adapter)) + .to receive(:redis).and_return(redis) end shared_examples_for 'serializer' do @@ -367,8 +372,8 @@ end it 'decodes correctly' do - expect(store.send(:get_session, env, session_id)) - .to eq([session_id, session_data]) + expect(store.send(:load_session_from_redis, env, session_id)) + .to eq(session_data) end end @@ -422,20 +427,21 @@ def self.dump(_value) describe 'handling decode errors' do context 'when a class is serialized that does not exist' do before do - allow(store).to receive(:redis) + allow(store.instance_variable_get(:@adapter)).to receive(:redis) .and_return(double('redis', get: "\x04\bo:\nNonExistentClass\x00", del: true)) end it 'returns an empty session' do - expect(store.send(:load_session_from_redis, 'whatever')).to be_nil + expect(store.send(:load_session_from_redis, double('env'), 'whatever')) + .to be_nil end it 'destroys and drops the session' do expect(store).to receive(:destroy_session_from_sid) .with('wut', drop: true) - store.send(:load_session_from_redis, 'wut') + store.send(:load_session_from_redis, double('env'), 'wut') end context 'when a custom on_session_load_error handler is provided' do @@ -447,7 +453,7 @@ def self.dump(_value) end it 'passes the error and the sid to the handler' do - store.send(:load_session_from_redis, 'foo') + store.send(:load_session_from_redis, double('env'), 'foo') expect(@e).to be_kind_of(StandardError) expect(@sid).to eq('foo') end @@ -456,18 +462,19 @@ def self.dump(_value) context 'when the encoded data is invalid' do before do - allow(store).to receive(:redis) + allow(store.instance_variable_get(:@adapter)).to receive(:redis) .and_return(double('redis', get: "\x00\x00\x00\x00", del: true)) end it 'returns an empty session' do - expect(store.send(:load_session_from_redis, 'bar')).to be_nil + expect(store.send(:load_session_from_redis, double('env'), 'bar')) + .to be_nil end it 'destroys and drops the session' do expect(store).to receive(:destroy_session_from_sid) .with('wut', drop: true) - store.send(:load_session_from_redis, 'wut') + store.send(:load_session_from_redis, double('env'), 'wut') end context 'when a custom on_session_load_error handler is provided' do @@ -479,7 +486,7 @@ def self.dump(_value) end it 'passes the error and the sid to the handler' do - store.send(:load_session_from_redis, 'foo') + store.send(:load_session_from_redis, double('env'), 'foo') expect(@e).to be_kind_of(StandardError) expect(@sid).to eq('foo') end