Skip to content

Add simple Redis SessionRepository implementation #1408

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

Closed
wants to merge 3 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* Copyright 2014-2019 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
*
* https://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 java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.UUID;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

/**
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {

@Autowired
private SimpleRedisOperationsSessionRepository sessionRepository;

@Test
void save_NewSession_ShouldSaveSession() {
RedisSession session = createAndSaveSession(Instant.now());
assertThat(session.getMaxInactiveInterval()).isEqualTo(
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
assertThat(session.getAttributeNames())
.isEqualTo(Collections.singleton("attribute1"));
assertThat(session.<String>getAttribute("attribute1")).isEqualTo("value1");
}

@Test
void save_LastAccessedTimeInPast_ShouldExpireSession() {
assertThat(createAndSaveSession(Instant.EPOCH)).isNull();
}

@Test
void save_DeletedSession_ShouldThrowException() {
RedisSession session = createAndSaveSession(Instant.now());
this.sessionRepository.deleteById(session.getId());
assertThatIllegalStateException()
.isThrownBy(() -> this.sessionRepository.save(session))
.withMessage("Session was invalidated");
}

@Test
void save_ConcurrentUpdates_ShouldSaveSession() {
RedisSession copy1 = createAndSaveSession(Instant.now());
String sessionId = copy1.getId();
RedisSession copy2 = this.sessionRepository.findById(sessionId);
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
updateSession(copy1, now.plusSeconds(1L), "attribute2", "value2");
this.sessionRepository.save(copy1);
updateSession(copy2, now.plusSeconds(2L), "attribute3", "value3");
this.sessionRepository.save(copy2);
RedisSession session = this.sessionRepository.findById(sessionId);
assertThat(session.getLastAccessedTime()).isEqualTo(now.plusSeconds(2L));
assertThat(session.getAttributeNames()).hasSize(3);
assertThat(session.<String>getAttribute("attribute1")).isEqualTo("value1");
assertThat(session.<String>getAttribute("attribute2")).isEqualTo("value2");
assertThat(session.<String>getAttribute("attribute3")).isEqualTo("value3");
}

@Test
void save_ChangeSessionIdAndUpdateAttribute_ShouldChangeSessionId() {
RedisSession session = createAndSaveSession(Instant.now());
String originalSessionId = session.getId();
updateSession(session, Instant.now(), "attribute1", "value2");
String newSessionId = session.changeSessionId();
this.sessionRepository.save(session);
RedisSession loaded = this.sessionRepository.findById(newSessionId);
assertThat(loaded).isNotNull();
assertThat(loaded.getAttributeNames()).hasSize(1);
assertThat(loaded.<String>getAttribute("attribute1")).isEqualTo("value2");
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_OnlyChangeSessionId_ShouldChangeSessionId() {
RedisSession session = createAndSaveSession(Instant.now());
String originalSessionId = session.getId();
String newSessionId = session.changeSessionId();
this.sessionRepository.save(session);
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_ChangeSessionIdTwice_ShouldChangeSessionId() {
RedisSession session = createAndSaveSession(Instant.now());
String originalSessionId = session.getId();
updateSession(session, Instant.now(), "attribute1", "value2");
String newSessionId1 = session.changeSessionId();
updateSession(session, Instant.now(), "attribute1", "value3");
String newSessionId2 = session.changeSessionId();
this.sessionRepository.save(session);
assertThat(this.sessionRepository.findById(newSessionId1)).isNull();
assertThat(this.sessionRepository.findById(newSessionId2)).isNotNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_ChangeSessionIdOnNewSession_ShouldChangeSessionId() {
RedisSession session = this.sessionRepository.createSession();
String originalSessionId = session.getId();
updateSession(session, Instant.now(), "attribute1", "value1");
String newSessionId = session.changeSessionId();
this.sessionRepository.save(session);
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_ChangeSessionIdSaveTwice_ShouldChangeSessionId() {
RedisSession session = createAndSaveSession(Instant.now());
String originalSessionId;
originalSessionId = session.getId();
updateSession(session, Instant.now(), "attribute1", "value1");
String newSessionId = session.changeSessionId();
this.sessionRepository.save(session);
this.sessionRepository.save(session);
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_ChangeSessionIdOnDeletedSession_ShouldThrowException() {
RedisSession session = createAndSaveSession(Instant.now());
String originalSessionId = session.getId();
this.sessionRepository.deleteById(originalSessionId);
updateSession(session, Instant.now(), "attribute1", "value1");
String newSessionId = session.changeSessionId();
assertThatIllegalStateException()
.isThrownBy(() -> this.sessionRepository.save(session))
.withMessage("Session was invalidated");
assertThat(this.sessionRepository.findById(newSessionId)).isNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void save_ChangeSessionIdConcurrent_ShouldThrowException() {
RedisSession copy1 = createAndSaveSession(Instant.now());
String originalSessionId = copy1.getId();
RedisSession copy2 = this.sessionRepository.findById(originalSessionId);
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
updateSession(copy1, now.plusSeconds(1L), "attribute2", "value2");
String newSessionId1 = copy1.changeSessionId();
this.sessionRepository.save(copy1);
updateSession(copy2, now.plusSeconds(2L), "attribute3", "value3");
String newSessionId2 = copy2.changeSessionId();
assertThatIllegalStateException()
.isThrownBy(() -> this.sessionRepository.save(copy2))
.withMessage("Session was invalidated");
assertThat(this.sessionRepository.findById(newSessionId1)).isNotNull();
assertThat(this.sessionRepository.findById(newSessionId2)).isNull();
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
}

@Test
void deleteById_ValidSession_ShouldDeleteSession() {
RedisSession session = createAndSaveSession(Instant.now());
this.sessionRepository.deleteById(session.getId());
assertThat(this.sessionRepository.findById(session.getId())).isNull();
}

@Test
void deleteById_DeletedSession_ShouldDoNothing() {
RedisSession session = createAndSaveSession(Instant.now());
this.sessionRepository.deleteById(session.getId());
this.sessionRepository.deleteById(session.getId());
assertThat(this.sessionRepository.findById(session.getId())).isNull();
}

@Test
void deleteById_NonexistentSession_ShouldDoNothing() {
String sessionId = UUID.randomUUID().toString();
this.sessionRepository.deleteById(sessionId);
assertThat(this.sessionRepository.findById(sessionId)).isNull();
}

private RedisSession createAndSaveSession(Instant lastAccessedTime) {
RedisSession session = this.sessionRepository.createSession();
session.setLastAccessedTime(lastAccessedTime);
session.setAttribute("attribute1", "value1");
this.sessionRepository.save(session);
return this.sessionRepository.findById(session.getId());
}

private static void updateSession(RedisSession session, Instant lastAccessedTime,
String attributeName, Object attributeValue) {
session.setLastAccessedTime(lastAccessedTime);
session.setAttribute(attributeName, attributeValue);
}

@Configuration
@EnableSpringHttpSession
static class Config extends BaseConfig {

@Bean
public SimpleRedisOperationsSessionRepository sessionRepository(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new SimpleRedisOperationsSessionRepository(redisTemplate);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
Expand All @@ -47,29 +46,6 @@ public class ReactiveRedisOperationsSessionRepository implements
*/
public static final String DEFAULT_NAMESPACE = "spring:session";

/**
* The key in the Hash representing {@link Session#getCreationTime()}.
*/
static final String CREATION_TIME_KEY = "creationTime";

/**
* The key in the Hash representing {@link Session#getLastAccessedTime()}.
*/
static final String LAST_ACCESSED_TIME_KEY = "lastAccessedTime";

/**
* The key in the Hash representing {@link Session#getMaxInactiveInterval()} .
*/
static final String MAX_INACTIVE_INTERVAL_KEY = "maxInactiveInterval";

/**
* The prefix of the key 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 ATTRIBUTE_PREFIX = "sessionAttr:";

private final ReactiveRedisOperations<String, Object> sessionRedisOperations;

/**
Expand Down Expand Up @@ -162,7 +138,7 @@ public Mono<RedisSession> findById(String id) {
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
.filter((map) -> !map.isEmpty())
.map(new SessionMapper(id))
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map(RedisSession::new)
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
Expand All @@ -177,7 +153,7 @@ public Mono<Void> deleteById(String id) {
}

private static String getAttributeKey(String attributeName) {
return ATTRIBUTE_PREFIX + attributeName;
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}

private String getSessionKey(String sessionId) {
Expand Down Expand Up @@ -206,10 +182,12 @@ final class RedisSession implements Session {
*/
RedisSession() {
this(new MapSession());
this.delta.put(CREATION_TIME_KEY, getCreationTime().toEpochMilli());
this.delta.put(MAX_INACTIVE_INTERVAL_KEY,
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY,
getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) getMaxInactiveInterval().getSeconds());
this.delta.put(LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
getLastAccessedTime().toEpochMilli());
this.isNew = true;
this.flushImmediateIfNecessary();
}
Expand Down Expand Up @@ -266,7 +244,8 @@ public Instant getCreationTime() {
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
putAndFlush(LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
putAndFlush(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
getLastAccessedTime().toEpochMilli());
}

@Override
Expand All @@ -277,7 +256,7 @@ public Instant getLastAccessedTime() {
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
putAndFlush(MAX_INACTIVE_INTERVAL_KEY,
putAndFlush(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) getMaxInactiveInterval().getSeconds());
}

Expand Down Expand Up @@ -354,36 +333,4 @@ private Mono<Void> saveChangeSessionId() {

}

private static final class SessionMapper
implements Function<Map<String, Object>, MapSession> {

private final String id;

private SessionMapper(String id) {
this.id = id;
}

@Override
public MapSession apply(Map<String, Object> map) {
MapSession session = new MapSession(this.id);

session.setCreationTime(
Instant.ofEpochMilli((long) map.get(CREATION_TIME_KEY)));
session.setLastAccessedTime(
Instant.ofEpochMilli((long) map.get(LAST_ACCESSED_TIME_KEY)));
session.setMaxInactiveInterval(
Duration.ofSeconds((int) map.get(MAX_INACTIVE_INTERVAL_KEY)));

map.forEach((name, value) -> {
if (name.startsWith(ATTRIBUTE_PREFIX)) {
session.setAttribute(name.substring(ATTRIBUTE_PREFIX.length()),
value);
}
});

return session;
}

}

}
Loading