Add ReactiveRedisIndexedSessionRepository
Closes gh-2700
This commit is contained in:
committed by
Marcus Hert Da Coregio
parent
3049318427
commit
e65da12d82
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import com.redis.testcontainers.RedisContainer;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
@@ -34,16 +34,17 @@ public abstract class AbstractRedisITests {
|
||||
protected static class BaseConfig {
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(6379);
|
||||
public RedisContainer redisContainer() {
|
||||
RedisContainer redisContainer = new RedisContainer(
|
||||
RedisContainer.DEFAULT_IMAGE_NAME.withTag(RedisContainer.DEFAULT_TAG));
|
||||
redisContainer.start();
|
||||
return redisContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LettuceConnectionFactory redisConnectionFactory() {
|
||||
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisContainer().getHost(),
|
||||
redisContainer().getFirstMappedPort());
|
||||
public LettuceConnectionFactory redisConnectionFactory(RedisContainer redisContainer) {
|
||||
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisContainer.getHost(),
|
||||
redisContainer.getFirstMappedPort());
|
||||
return new LettuceConnectionFactory(configuration);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2014-2023 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.Comparator;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
|
||||
import org.springframework.session.data.SessionEventRegistry;
|
||||
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.config.ConfigureReactiveRedisAction;
|
||||
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisIndexedWebSession;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class ReactiveRedisIndexedSessionRepositoryConfigurationITests {
|
||||
|
||||
ReactiveRedisIndexedSessionRepository repository;
|
||||
|
||||
ReactiveRedisOperations<String, Object> sessionRedisOperations;
|
||||
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
|
||||
SecurityContext securityContext;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.securityContext = SecurityContextHolder.createEmptyContext();
|
||||
this.securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(),
|
||||
"na", AuthorityUtils.createAuthorityList("ROLE_USER")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanUpTaskWhenSessionIsExpiredThenAllRelatedKeysAreDeleted() {
|
||||
registerConfig(OneSecCleanUpIntervalConfig.class);
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute("SPRING_SECURITY_CONTEXT", this.securityContext);
|
||||
this.repository.save(session).block();
|
||||
await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> {
|
||||
assertThat(this.repository.findById(session.getId()).block()).isNull();
|
||||
Boolean hasSessionKey = this.sessionRedisOperations.hasKey("spring:session:sessions:" + session.getId())
|
||||
.block();
|
||||
Boolean hasSessionIndexesKey = this.sessionRedisOperations
|
||||
.hasKey("spring:session:sessions:" + session.getId() + ":idx")
|
||||
.block();
|
||||
Boolean hasPrincipalIndexKey = this.sessionRedisOperations
|
||||
.hasKey("spring:session:sessions:index:"
|
||||
+ ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
|
||||
+ this.securityContext.getAuthentication().getName())
|
||||
.block();
|
||||
Long expirationsSize = this.sessionRedisOperations.opsForZSet()
|
||||
.size("spring:session:sessions:expirations")
|
||||
.block();
|
||||
assertThat(hasSessionKey).isFalse();
|
||||
assertThat(hasSessionIndexesKey).isFalse();
|
||||
assertThat(hasPrincipalIndexKey).isFalse();
|
||||
assertThat(expirationsSize).isZero();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void onSessionCreatedWhenUsingJsonSerializerThenEventDeserializedCorrectly() throws InterruptedException {
|
||||
registerConfig(SessionEventRegistryJsonSerializerConfig.class);
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
this.repository.save(session).block();
|
||||
SessionEventRegistry registry = this.context.getBean(SessionEventRegistry.class);
|
||||
SessionCreatedEvent event = registry.getEvent(session.getId());
|
||||
Session eventSession = event.getSession();
|
||||
assertThat(eventSession).usingRecursiveComparison()
|
||||
.withComparatorForFields(new InstantComparator(), "cached.creationTime", "cached.lastAccessedTime")
|
||||
.isEqualTo(session);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionExpiredWhenNoCleanUpTaskAndNoKeyspaceEventsThenNoCleanup() {
|
||||
registerConfig(DisableCleanupTaskAndNoKeyspaceEventsConfig.class);
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
this.repository.save(session).block();
|
||||
await().during(Duration.ofSeconds(3)).untilAsserted(() -> {
|
||||
Boolean exists = this.sessionRedisOperations.hasKey("spring:session:sessions:" + session.getId()).block();
|
||||
assertThat(exists).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
private void registerConfig(Class<?> clazz) {
|
||||
this.context.register(clazz);
|
||||
this.context.refresh();
|
||||
this.repository = this.context.getBean(ReactiveRedisIndexedSessionRepository.class);
|
||||
this.sessionRedisOperations = this.repository.getSessionRedisOperations();
|
||||
}
|
||||
|
||||
static class InstantComparator implements Comparator<Instant> {
|
||||
|
||||
@Override
|
||||
public int compare(Instant o1, Instant o2) {
|
||||
return o1.truncatedTo(ChronoUnit.SECONDS).compareTo(o2.truncatedTo(ChronoUnit.SECONDS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableRedisIndexedWebSession(maxInactiveIntervalInSeconds = 1)
|
||||
@Import(AbstractRedisITests.BaseConfig.class)
|
||||
static class OneSecCleanUpIntervalConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> customizer() {
|
||||
return (sessionRepository) -> sessionRepository.setCleanupInterval(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableRedisIndexedWebSession
|
||||
@Import(AbstractRedisITests.BaseConfig.class)
|
||||
static class SessionEventRegistryJsonSerializerConfig {
|
||||
|
||||
@Bean
|
||||
SessionEventRegistry sessionEventRegistry() {
|
||||
return new SessionEventRegistry();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
return RedisSerializer.json();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableRedisIndexedWebSession(maxInactiveIntervalInSeconds = 1)
|
||||
@Import(AbstractRedisITests.BaseConfig.class)
|
||||
static class DisableCleanupTaskAndNoKeyspaceEventsConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> customizer() {
|
||||
return ReactiveRedisIndexedSessionRepository::disableCleanupTask;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ConfigureReactiveRedisAction configureReactiveRedisAction() {
|
||||
return (connection) -> connection.serverCommands().setConfig("notify-keyspace-events", "").then();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
/*
|
||||
* Copyright 2014-2023 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.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.context.annotation.Import;
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.data.SessionEventRegistry;
|
||||
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
|
||||
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisIndexedWebSession;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
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.assertThatNoException;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
@SuppressWarnings({ "ConstantConditions" })
|
||||
class ReactiveRedisIndexedSessionRepositoryITests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final String INDEX_NAME = ReactiveRedisIndexedSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
|
||||
|
||||
@Autowired
|
||||
private ReactiveRedisIndexedSessionRepository repository;
|
||||
|
||||
@Autowired
|
||||
private SessionEventRegistry eventRegistry;
|
||||
|
||||
@SpringSessionRedisOperations
|
||||
private ReactiveRedisOperations<Object, Object> redis;
|
||||
|
||||
private SecurityContext context;
|
||||
|
||||
private SecurityContext changedContext;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.context = SecurityContextHolder.createEmptyContext();
|
||||
this.context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER")));
|
||||
|
||||
this.changedContext = SecurityContextHolder.createEmptyContext();
|
||||
this.changedContext.setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||
"changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByIdWhenSavedThenFound() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute("foo", "bar");
|
||||
this.repository.save(session).block();
|
||||
RedisSession savedSession = this.repository.findById(session.getId()).block();
|
||||
assertThat(savedSession).isNotNull();
|
||||
assertThat(savedSession.getId()).isEqualTo(session.getId());
|
||||
assertThat(savedSession.<String>getAttribute("foo")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenHasSecurityContextAttributeThenPrincipalIndexKeySaved() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute("foo", "bar");
|
||||
|
||||
String username = "user";
|
||||
Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(username, "password",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(authentication);
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, context);
|
||||
this.repository.save(session).block();
|
||||
|
||||
String usernameSessionKey = "spring:session:sessions:index:"
|
||||
+ ReactiveRedisIndexedSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + username;
|
||||
Boolean sessionExistsOnPrincipalKey = this.redis.opsForSet()
|
||||
.isMember(usernameSessionKey, session.getId())
|
||||
.block();
|
||||
assertThat(sessionExistsOnPrincipalKey).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWhenSuccessThenSessionCreatedEvent() throws InterruptedException {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute("foo", "bar");
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
SessionCreatedEvent event = this.eventRegistry.getEvent(session.getId());
|
||||
assertThat(event).isNotNull();
|
||||
RedisSession eventSession = event.getSession();
|
||||
compareSessions(session, eventSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenExistsThenReturn() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
String principalName = "principal";
|
||||
session.setAttribute(ReactiveRedisIndexedSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> principalSessions = this.repository
|
||||
.findByIndexNameAndIndexValue(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
principalName)
|
||||
.block();
|
||||
|
||||
assertThat(principalSessions).hasSize(1);
|
||||
assertThat(principalSessions.keySet()).containsOnly(session.getId());
|
||||
|
||||
this.repository.deleteById(session.getId()).block();
|
||||
|
||||
principalSessions = this.repository
|
||||
.findByIndexNameAndIndexValue(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
principalName)
|
||||
.block();
|
||||
assertThat(principalSessions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenExpireKeyEventThenRemovesIndexAndSessionExpiredEvent() {
|
||||
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
this.eventRegistry.clear();
|
||||
String key = "spring:session:sessions:expires:" + toSave.getId();
|
||||
assertThat(this.redis.expire(key, Duration.ofSeconds(1)).block()).isTrue();
|
||||
|
||||
await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> {
|
||||
SessionExpiredEvent event = this.eventRegistry.getEvent(toSave.getId());
|
||||
RedisSession eventSession = event.getSession();
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).hasSize(0);
|
||||
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
|
||||
assertThat(event).isNotNull();
|
||||
compareSessions(toSave, eventSession);
|
||||
});
|
||||
}
|
||||
|
||||
private static void compareSessions(RedisSession session1, RedisSession session2) {
|
||||
assertThat(session2.getCreationTime().truncatedTo(ChronoUnit.SECONDS))
|
||||
.isEqualTo(session1.getCreationTime().truncatedTo(ChronoUnit.SECONDS));
|
||||
assertThat(session2.getMaxInactiveInterval().truncatedTo(ChronoUnit.SECONDS))
|
||||
.isEqualTo(session1.getMaxInactiveInterval().truncatedTo(ChronoUnit.SECONDS));
|
||||
assertThat(session2.getId()).isEqualTo(session1.getId());
|
||||
assertThat(session2.getAttributeNames()).isEqualTo(session1.getAttributeNames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenDeletedKeyEventThenRemovesIndex() {
|
||||
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
String key = "spring:session:sessions:expires:" + toSave.getId();
|
||||
assertThat(this.redis.delete(key).block()).isEqualTo(1);
|
||||
|
||||
await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> {
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).hasSize(0);
|
||||
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
|
||||
SessionDeletedEvent event = this.eventRegistry.getEvent(toSave.getId());
|
||||
assertThat(event).isNotNull();
|
||||
RedisSession eventSession = event.getSession();
|
||||
compareSessions(toSave, eventSession);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenNoPrincipalNameChangeThenKeepIndex() {
|
||||
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenNoPrincipalNameChangeAndFindByIdThenKeepIndex() {
|
||||
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
toSave = this.repository.findById(toSave.getId()).block();
|
||||
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenDeletedPrincipalAttributeThenEmpty() {
|
||||
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
toSave.removeAttribute(INDEX_NAME);
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenDeletedPrincipalAttributeAndFindByIdThenEmpty() {
|
||||
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(session).block();
|
||||
session = this.repository.findById(session.getId()).block();
|
||||
|
||||
session.removeAttribute(INDEX_NAME);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenChangedSecurityContextAttributeThenIndexMovedToNewPrincipal() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
String principalNameChanged = this.changedContext.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenChangedSecurityContextAttributeAndFindByIdThenIndexMovedToNewPrincipal() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
String principalNameChanged = this.changedContext.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
session = this.repository.findById(session.getId()).block();
|
||||
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenNoSecurityContextChangeThenKeepIndex() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
session.setAttribute("other", "value");
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenNoSecurityContextChangeAndFindByIdThenKeepIndex() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
toSave = this.repository.findById(toSave.getId()).block();
|
||||
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenDeletedSecurityContextAttributeThenEmpty() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
toSave.removeAttribute(SPRING_SECURITY_CONTEXT);
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenDeletedSecurityContextAttributeAndFindByIdThenEmpty() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
session = this.repository.findById(session.getId()).block();
|
||||
|
||||
session.removeAttribute(SPRING_SECURITY_CONTEXT);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
|
||||
.block();
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenChangedPrincipalAttributeThenEmpty() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
String principalNameChanged = this.changedContext.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameWhenChangedPrincipalAttributeAndFindByIdThenEmpty() {
|
||||
String principalName = this.context.getAuthentication().getName();
|
||||
String principalNameChanged = this.changedContext.getAuthentication().getName();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
session = this.repository.findById(session.getId()).block();
|
||||
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(session).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(session.getId());
|
||||
}
|
||||
|
||||
// gh-1791
|
||||
@Test
|
||||
void changeSessionIdWhenSessionExpiredThenRemovesAllPrincipalIndex() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
String usernameSessionKey = "spring:session:sessions:index:" + INDEX_NAME + ":" + getSecurityName();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
String originalFindById = findById.getId();
|
||||
|
||||
assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block()).contains(originalFindById);
|
||||
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
findById.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(findById).block();
|
||||
|
||||
assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block()).contains(changeSessionId);
|
||||
|
||||
String key = "spring:session:sessions:expires:" + changeSessionId;
|
||||
assertThat(this.redis.expire(key, Duration.ofSeconds(1)).block()).isTrue(); // expire
|
||||
// the
|
||||
// key
|
||||
|
||||
await().atMost(Duration.ofSeconds(5))
|
||||
.untilAsserted(() -> assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block())
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenSessionDeletedThenRemovesAllPrincipalIndex() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(session).block();
|
||||
String usernameSessionKey = "spring:session:sessions:index:" + INDEX_NAME + ":" + getSecurityName();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
String originalFindById = findById.getId();
|
||||
|
||||
assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block()).contains(originalFindById);
|
||||
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
findById.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(findById).block();
|
||||
|
||||
assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block()).contains(changeSessionId);
|
||||
|
||||
String key = "spring:session:sessions:expires:" + changeSessionId;
|
||||
assertThat(this.redis.delete(key).block()).isEqualTo(1);
|
||||
|
||||
await().atMost(Duration.ofSeconds(5))
|
||||
.untilAsserted(() -> assertThat(this.redis.opsForSet().members(usernameSessionKey).collectList().block())
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenPrincipalNameChangesThenNewPrincipalMapsToNewSessionId() {
|
||||
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
findById.setAttribute(INDEX_NAME, principalNameChanged);
|
||||
this.repository.save(findById).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(changeSessionId);
|
||||
}
|
||||
|
||||
// gh-1987
|
||||
@Test
|
||||
void changeSessionIdWhenPrincipalNameChangesFromNullThenIndexShouldNotBeCreated() {
|
||||
String principalName = null;
|
||||
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
findById.setAttribute(INDEX_NAME, principalNameChanged);
|
||||
this.repository.save(findById).block();
|
||||
|
||||
Map<String, RedisSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
|
||||
.block();
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged).block();
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(changeSessionId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenOnlyChangeId() {
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute(attrName, attrValue);
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
|
||||
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
|
||||
String originalFindById = findById.getId();
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
|
||||
this.repository.save(findById).block();
|
||||
|
||||
assertThat(this.repository.findById(originalFindById).block()).isNull();
|
||||
|
||||
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
|
||||
|
||||
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenChangeTwice() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
String originalId = session.getId();
|
||||
String changeId1 = session.changeSessionId();
|
||||
String changeId2 = session.changeSessionId();
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
assertThat(this.repository.findById(originalId).block()).isNull();
|
||||
assertThat(this.repository.findById(changeId1).block()).isNull();
|
||||
assertThat(this.repository.findById(changeId2).block()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenSetAttributeOnChangedSession() {
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
RedisSession findById = this.repository.findById(session.getId()).block();
|
||||
|
||||
findById.setAttribute(attrName, attrValue);
|
||||
|
||||
String originalFindById = findById.getId();
|
||||
String changeSessionId = findById.changeSessionId();
|
||||
|
||||
this.repository.save(findById).block();
|
||||
|
||||
assertThat(this.repository.findById(originalFindById).block()).isNull();
|
||||
|
||||
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
|
||||
|
||||
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenHasNotSaved() {
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
String originalId = session.getId();
|
||||
session.changeSessionId();
|
||||
|
||||
this.repository.save(session).block();
|
||||
|
||||
assertThat(this.repository.findById(session.getId()).block()).isNotNull();
|
||||
assertThat(this.repository.findById(originalId).block()).isNull();
|
||||
}
|
||||
|
||||
// gh-962
|
||||
@Test
|
||||
void changeSessionIdSaveTwice() {
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String originalId = toSave.getId();
|
||||
toSave.changeSessionId();
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
assertThat(this.repository.findById(toSave.getId()).block()).isNotNull();
|
||||
assertThat(this.repository.findById(originalId).block()).isNull();
|
||||
}
|
||||
|
||||
// gh-1137
|
||||
@Test
|
||||
void changeSessionIdWhenSessionIsDeleted() {
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String sessionId = toSave.getId();
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
this.repository.deleteById(sessionId).block();
|
||||
|
||||
toSave.changeSessionId();
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
assertThat(this.repository.findById(toSave.getId()).block()).isNull();
|
||||
assertThat(this.repository.findById(sessionId).block()).isNull();
|
||||
}
|
||||
|
||||
@Test // gh-1270
|
||||
void changeSessionIdSaveConcurrently() {
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String originalId = toSave.getId();
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
RedisSession copy1 = this.repository.findById(originalId).block();
|
||||
RedisSession copy2 = this.repository.findById(originalId).block();
|
||||
|
||||
copy1.changeSessionId();
|
||||
this.repository.save(copy1).block();
|
||||
copy2.changeSessionId();
|
||||
this.repository.save(copy2).block();
|
||||
|
||||
assertThat(this.repository.findById(originalId).block()).isNull();
|
||||
assertThat(this.repository.findById(copy1.getId()).block()).isNotNull();
|
||||
assertThat(this.repository.findById(copy2.getId()).block()).isNull();
|
||||
}
|
||||
|
||||
// gh-1743
|
||||
@Test
|
||||
void saveChangeSessionIdWhenFailedRenameOperationExceptionContainsMoreDetailsThenIgnoreError() {
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String sessionId = toSave.getId();
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
RedisSession session = this.repository.findById(sessionId).block();
|
||||
this.repository.deleteById(sessionId).block();
|
||||
session.changeSessionId();
|
||||
|
||||
assertThatNoException().isThrownBy(() -> this.repository.save(session).block());
|
||||
}
|
||||
|
||||
private String getSecurityName() {
|
||||
return this.context.getAuthentication().getName();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableRedisIndexedWebSession
|
||||
@Import(AbstractRedisITests.BaseConfig.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
SessionEventRegistry sessionEventRegistry() {
|
||||
return new SessionEventRegistry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user