Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added: adapter for java spring mvc #80

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.gem
*.rbc
*.swp
.bundle
.config
.yardoc
Expand Down
142 changes: 108 additions & 34 deletions lib/redis-session-store.rb
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
#
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
37 changes: 22 additions & 15 deletions spec/redis_session_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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}")

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down