/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.redis;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
*
* A {@link org.springframework.session.SessionRepository} that is implemented using
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
* . This implementation supports {@link SessionDeletedEvent} and
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
*
*
*
Creating a new instance
*
* A typical example of how to create a new instance can be seen below:
*
*
* JedisConnectionFactory factory = new JedisConnectionFactory();
*
* RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(factory);
*
*
* The sections below outline how Redis is updated for each operation. An example of
* creating a new session can be found below. The subsequent sections describe the
* details.
*
*
* Each session is stored in Redis as a
* Hash. Each session is set and
* updated using the HMSET command. An
* example of how each session is stored can be seen below.
*
* In this example, the session following statements are true about the session:
*
*
*
The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe
*
The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
* GMT.
*
The session expires in 1800 seconds (30 minutes).
*
The session was last accessed at 1404360000000 in milliseconds since midnight of
* 1/1/1970 GMT.
*
The session has two attributes. The first is "attrName" with the value of
* "someAttrValue". The second session attribute is named "attrName2" with the value of
* "someAttrValue2".
*
*
*
*
Optimized Writes
*
*
* The {@link RedisSession} keeps track of the properties that have changed and only
* updates those. This means if an attribute is written once and read many times we only
* need to write that attribute once. For example, assume the session attribute
* "sessionAttr2" from earlier was updated. The following would be executed upon saving:
*
* When a session is created an event is sent to Redis with the channel of
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the event will be
* the session that was created.
*
*
*
* If registered as a {@link MessageListener}, then
* {@link RedisOperationsSessionRepository} will then translate the Redis message into a
* {@link SessionCreatedEvent}.
*
*
*
Expiration
*
*
* An expiration is associated to each session using the
* EXPIRE command based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveIntervalInSeconds()}
* . For example:
*
* You will note that the expiration that is set is 5 minutes after the session actually
* expires. This is necessary so that the value of the session can be accessed when the
* session expires. An expiration is set on the session itself five minutes after it
* actually expires to ensure it is cleaned up, but only after we perform any necessary
* processing.
*
*
*
* NOTE: The {@link #getSession(String)} method ensures that no expired sessions
* will be returned. This means there is no need to check the expiration before using a
* session
*
*
*
* Spring Session relies on the expired and delete
* keyspace notifications from Redis to
* fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
* associated with the Session are cleaned up. For example, when using Spring Session's
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
* connections associated with the session to be closed.
*
*
*
* Expiration is not tracked directly on the session key itself since this would mean the
* session data would no longer be available. Instead a special session expires key is
* used. In our example the expires key is:
*
* When a session expires key is deleted or expires, the keyspace notification triggers a
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
*
*
*
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if they key has not been accessed.
* Specifically the background task that Redis uses to clean up expired keys is a low
* priority task and may not trigger the key expiration. For additional details see
* Timing of expired events section in
* the Redis documentation.
*
*
*
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
* that each key is accessed when it is expected to expire. This means that if the TTL is
* expired on the key, Redis will remove the key and fire the expired event when we try to
* access they key.
*
*
*
* For this reason, each session expiration is also tracked to the nearest minute. This
* allows a background task to access the potentially expired sessions to ensure that
* Redis expired events are fired in a more deterministic fashion. For example:
*
* The background task will then use these mappings to explicitly request each session
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
* deletes the key for us only if the TTL is expired.
*
*
* NOTE: We do not explicitly delete the keys since in some instances there may be
* a race condition that incorrectly identifies a key as expired when it is not. Short of
* using distributed locks (which would kill our performance) there is no way to ensure
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
* the key is only removed if the TTL on that key is expired.
*
*
* @author Rob Winch
* @since 1.0
*/
public class RedisOperationsSessionRepository implements
FindByIndexNameSessionRepository,
MessageListener {
private static final Log logger = LogFactory
.getLog(RedisOperationsSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
static PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
/**
* The default prefix for each key and channel in Redis used by Spring Session.
*/
static final String DEFAULT_SPRING_SESSION_REDIS_PREFIX = "spring:session:";
/**
* The key in the Hash representing
* {@link org.springframework.session.ExpiringSession#getCreationTime()}.
*/
static final String CREATION_TIME_ATTR = "creationTime";
/**
* The key in the Hash representing
* {@link org.springframework.session.ExpiringSession#getMaxInactiveIntervalInSeconds()}
* .
*/
static final String MAX_INACTIVE_ATTR = "maxInactiveInterval";
/**
* The key in the Hash representing
* {@link org.springframework.session.ExpiringSession#getLastAccessedTime()}.
*/
static final String LAST_ACCESSED_ATTR = "lastAccessedTime";
/**
* The prefix of the key for used for session attributes. The suffix is the name of
* the session attribute. For example, if the session contained an attribute named
* attributeName, then there would be an entry in the hash named
* sessionAttr:attributeName that mapped to its value.
*/
static final String SESSION_ATTR_PREFIX = "sessionAttr:";
/**
* The prefix for every key used by Spring Session in Redis.
*/
private String keyPrefix = DEFAULT_SPRING_SESSION_REDIS_PREFIX;
private final RedisOperations