Compare commits

...

39 Commits

Author SHA1 Message Date
Eleftheria Stein
8af09781a0 Release 2.6.0-RC1 2021-10-19 11:38:25 +02:00
Eleftheria Stein
845c7aca84 Upgrade test dependencies 2021-10-19 11:14:03 +02:00
Eleftheria Stein
b05575722c Upgrade samples to Spring Boot 2.5.5
Closes gh-1929
2021-10-19 10:54:01 +02:00
Eleftheria Stein
ee0e03b91e Upgrade Spring Security to 5.6.0-RC1
Closes gh-1928
2021-10-19 10:39:06 +02:00
Eleftheria Stein
7864f9c4cc Upgrade Spring Framework to 5.3.11
Closes gh-1927
2021-10-19 10:37:19 +02:00
Eleftheria Stein
227aee8e3a Upgrade Reactor to 2020.0.12
Closes gh-1925
2021-10-19 10:36:09 +02:00
Eleftheria Stein
bf2aaa0033 Upgrade Spring Data to 2021.1.0-RC1
Closes gh-1926
2021-10-19 10:35:39 +02:00
Eleftheria Stein
eb9f62a437 Update principal index on session ID change
Closes gh-1791
2021-10-14 17:49:56 +02:00
Rob Winch
418cb60f39 Add antora gradle plugin
You can now run the following to generate the antora site

./gradlew antora

It will appear at build/site/index.html
2021-10-11 09:17:50 -05:00
Rob Winch
4339b8ae9d Fix local-antora-playbook.yml
- Point to generated content
- Point to remote antora-ui-spring
2021-10-11 09:16:55 -05:00
Eleftheria Stein
63f706dbf9 Fix Hazelcast session with flush mode immediate
Closes gh-1921
2021-10-05 10:59:06 +02:00
Eleftheria Stein
beb7b334c4 Fix link to Spring Security remember-me docs
Closes gh-1915
2021-10-04 16:34:52 +02:00
Eleftheria Stein
a64a11ba03 Tests for Hazelcast flush mode immediate
Closes gh-1801
2021-10-01 17:02:33 +02:00
slondono
661ecaf371 Store Principal Name Index in the Hazelcast Session delta
Issue gh-1801
2021-10-01 17:02:33 +02:00
Rob Winch
378ba6db2c Use GH_ACTIONS_REPO_TOKEN 2021-09-27 13:22:20 -05:00
zhaokai
9659f1f571 Modify to support negative numbers 2021-09-27 14:40:16 +02:00
Eleftheria Stein
919a2a5c49 Upgrade back to Spring Boot 2.5.3 2021-09-24 16:46:32 +02:00
Rob Winch
4dee8063c6 Use Antora
Closes gh-1237
2021-09-23 16:44:39 -05:00
Eleftheria Stein
9ad871a30b Add setter for autowired field in SpringWebSessionConfiguration
Closes gh-1918
2021-09-23 14:54:22 +02:00
Eleftheria Stein
e7d58f6b03 Increase session timeout in Hazelcast tests
It's possible that the session is expiring before the assertions can be made in the tests, causing them to fail.

Issue gh-1912
2021-09-08 12:04:26 +02:00
Rob Winch
3d118242ee Better hiearchy with Samples nav 2021-08-31 10:38:54 -05:00
Rob Winch
0c00ff0598 Fix Samples nav 2021-08-30 19:12:50 -05:00
Rob Winch
3d93bfc28b Fix Boot Samples Nav 2021-08-30 19:08:46 -05:00
Rob Winch
297ff83775 Added missing versions 2021-08-30 19:07:17 -05:00
Rob Winch
1fc2c430f1 Fix antora name 2021-08-30 19:01:52 -05:00
Rob Winch
5757e94658 Generated antora.yml 2021-08-30 18:55:11 -05:00
Rob Winch
8cc22a1712 Use versionless URL 2021-08-30 17:17:34 -05:00
Vedran Pavic
79fbca24eb Make Hazelcast session repository bean factory return type more specific
The declared return type of Hazelcast session repository bean factory method (i.e. HazelcastHttpSessionConfiguration#sessionRepository) was changed to SessionRepository<?> when support for Hazelcast 4 was added. This breaks Spring Boot's ability to auto-configure sessions endpoint, which is @ConditionalOnBean(FindByIndexNameSessionRepository.class), as the current return type is not specific enough to satisfy this condition.

This commit changes the return type of Hazelcast session repository bean factory method to FindByIndexNameSessionRepository<?>.

Closes: gh-1905
2021-08-27 01:51:55 +02:00
Vedran Pavic
5b7aee7199 Fix Spring Boot based Hazelcast samples
This commit removes unused Hazelcast client dependencies and test support from Spring Boot based Hazelcast samples.

Closes: gh-1902
2021-08-27 00:46:23 +02:00
Andreas Kasparek
c5bffde790 Always set time-to-live within entry processor
Closes gh-1899
2021-08-25 13:16:08 +02:00
Rob Winch
aee65ffec8 Remove :toc: left
This causes an extra toc that covers the left navigation
2021-08-20 14:29:21 -05:00
Rob Winch
00abd345ac Add Dispatch to build reference 2021-08-18 11:27:45 -05:00
Rob Winch
0864140dda Clean up introduction 2021-08-18 11:19:21 -05:00
Rob Winch
7babddf15f Fix default xref text 2021-08-18 11:16:36 -05:00
Rob Winch
764fc4eea6 <<>> to xref 2021-08-18 11:15:27 -05:00
Rob Winch
26419e2149 Cleanup Antora 2021-08-18 11:10:33 -05:00
Eleftheria Stein
585d3695ad Point to spring-session tag in GitHub issue template
Issue: gh-1897
2021-08-18 14:06:12 +02:00
Eleftheria Stein
db8a3aa604 Next development version 2021-08-17 15:52:29 +02:00
Rob Winch
faa6c441fa Antora 2021-08-16 15:44:15 -05:00
85 changed files with 2645 additions and 890 deletions

17
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,17 @@
<!--
!!! For Security Vulnerabilities, please go to https://spring.io/security-policy !!!
-->
**Affects:** \<Spring Framework version>
---
<!--
Thanks for taking the time to create an issue. Please read the following:
- Questions should be asked on Stack Overflow.
- For bugs, specify affected versions and explain what you are trying to do.
- For enhancements, provide context and describe the problem.
Issue or Pull Request? Create only one, not both. GitHub treats them as the same.
If unsure, start with an issue, and if you submit a pull request later, the
issue will be closed as superseded.
-->

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Support
url: https://stackoverflow.com/questions/tagged/spring-security
url: https://stackoverflow.com/questions/tagged/spring-session
about: Please ask and answer questions on StackOverflow with the tag spring-session

5
.github/actions/dispatch.sh vendored Executable file
View File

@@ -0,0 +1,5 @@
REPOSITORY_REF="$1"
TOKEN="$2"
curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches
echo "Requested Build for $REPOSITORY_REF"

27
.github/workflows/build-reference.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: reference
on:
push:
branches-ignore:
- 'gh-pages'
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Generate antora.yml
run: ./gradlew :spring-session-docs:generateAntora
- name: Push generated antora files to the spring-security-docs-generated
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: "spring-session/main" # The branch the action should deploy to.
folder: "spring-session-docs/build/generateAntora" # The folder the action should deploy.
repository-name: "spring-io/spring-generated-docs"
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Dispatch Build Request
run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'rwinch/spring-reference' "$GH_ACTIONS_REPO_TOKEN"

View File

@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

View File

@@ -4,7 +4,7 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.5.3'
springBootVersion = '2.5.5'
}
repositories {
@@ -27,11 +27,23 @@ buildscript {
}
}
plugins {
id "io.github.rwinch.antora" version "0.0.2"
}
apply plugin: 'io.spring.convention.root'
group = 'org.springframework.session'
description = 'Spring Session'
antora {
playbookFile = file("local-antora-playbook.yml")
// default no version (current version)
antoraVersion = "3.0.0-alpha.9"
arguments = ["--fetch"]
}
subprojects {
apply plugin: 'io.spring.javaformat'

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
version=2.6.0-M1
version=2.6.0-RC1

View File

@@ -1,10 +1,10 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:2020.0.10'
mavenBom 'org.junit:junit-bom:5.7.2'
mavenBom 'org.springframework:spring-framework-bom:5.3.9'
mavenBom 'org.springframework.data:spring-data-bom:2021.1.0-M2'
mavenBom 'org.springframework.security:spring-security-bom:5.6.0-M2'
mavenBom 'io.projectreactor:reactor-bom:2020.0.12'
mavenBom 'org.junit:junit-bom:5.8.1'
mavenBom 'org.springframework:spring-framework-bom:5.3.11'
mavenBom 'org.springframework.data:spring-data-bom:2021.1.0-RC1'
mavenBom 'org.springframework.security:spring-security-bom:5.6.0-RC1'
mavenBom 'org.testcontainers:testcontainers-bom:1.16.0'
}
@@ -18,19 +18,19 @@ dependencyManagement {
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.6.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:9.4.0.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:21.1.0.0'
dependency 'com.oracle.database.jdbc:ojdbc8:21.3.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:6.1.4.RELEASE'
dependency 'io.lettuce:lettuce-core:6.1.5.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:4.0.1'
dependency 'junit:junit:4.13.2'
dependency 'mysql:mysql-connector-java:8.0.26'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.20.2'
dependency 'org.hsqldb:hsqldb:2.5.1'
dependency 'org.assertj:assertj-core:3.21.0'
dependency 'org.hsqldb:hsqldb:2.5.2'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.4'
dependency 'org.mockito:mockito-core:3.11.2'
dependency 'org.postgresql:postgresql:42.2.23'
dependency 'org.mockito:mockito-core:4.0.0'
dependency 'org.postgresql:postgresql:42.2.24'
}
}

14
local-antora-playbook.yml Normal file
View File

@@ -0,0 +1,14 @@
site:
title: Spring Session
start_page: session::index.adoc
content:
sources:
- url: https://github.com/spring-io/spring-generated-docs
branches: [spring-session/main]
- url: ./
branches: HEAD
start_path: spring-session-docs
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true

View File

@@ -38,12 +38,13 @@ import org.springframework.web.server.session.WebSessionManager;
@Configuration(proxyBeanMethods = false)
public class SpringWebSessionConfiguration {
/**
* Optional override of default {@link WebSessionIdResolver}.
*/
@Autowired(required = false)
private WebSessionIdResolver webSessionIdResolver;
@Autowired(required = false)
public void setWebSessionIdResolver(WebSessionIdResolver webSessionIdResolver) {
this.webSessionIdResolver = webSessionIdResolver;
}
/**
* Configure a {@link WebSessionManager} using a provided
* {@link ReactiveSessionRepository}.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -473,6 +473,60 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test // gh-1791
void changeSessionIdWhenSessionExpiresThenRemovesAllPrincipalIndexIds() {
RedisSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
String usernameSessionKey = "RedisIndexedSessionRepositoryITests:index:" + INDEX_NAME + ":" + getSecurityName();
RedisSession findById = this.repository.findById(toSave.getId());
String originalFindById = findById.getId();
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).contains(originalFindById);
String changeSessionId = findById.changeSessionId();
findById.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(findById);
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).contains(changeSessionId);
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + changeSessionId;
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
byte[] pattern = new byte[] {};
this.repository.onMessage(message, pattern);
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).isEmpty();
}
@Test
void changeSessionIdWhenPrincipalNameChangesThenNewPrincipalMapsToNewSessionId() {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
RedisSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
RedisSession findById = this.repository.findById(toSave.getId());
String changeSessionId = findById.changeSessionId();
findById.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(findById);
Map<String, RedisSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(changeSessionId);
}
@Test
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -37,6 +37,7 @@ import org.springframework.util.Assert;
* {@link ReactiveRedisOperations}.
*
* @author Vedran Pavic
* @author Kai Zhao
* @since 2.2.0
*/
public class ReactiveRedisSessionRepository
@@ -274,8 +275,14 @@ public class ReactiveRedisSessionRepository
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisSessionRepository.this.sessionRedisOperations.opsForHash()
.putAll(sessionKey, new HashMap<>(this.delta));
Mono<Boolean> setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
getMaxInactiveInterval());
Mono<Boolean> setTtl;
if (getMaxInactiveInterval().getSeconds() >= 0) {
setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
getMaxInactiveInterval());
}
else {
setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.persist(sessionKey);
}
return update.and(setTtl).and((s) -> {
this.delta.clear();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -858,6 +858,11 @@ public class RedisIndexedSessionRepository
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(this.originalSessionId);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.add(sessionId);
}
this.originalSessionId = sessionId;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -56,6 +56,7 @@ import org.springframework.web.server.session.WebSessionManager;
* More advanced configurations can extend {@link RedisWebSessionConfiguration} instead.
*
* @author Vedran Pavic
* @author Kai Zhao
* @since 2.0.0
* @see EnableSpringWebSession
*/
@@ -68,7 +69,7 @@ public @interface EnableRedisWebSession {
/**
* The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
* This should be a non-negative integer.
* A negative number means permanently valid.
* @return the seconds a session can be inactive before expiring
*/
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -49,6 +49,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* Tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
* @author Kai Zhao
*/
class ReactiveRedisSessionRepositoryTests {
@@ -150,6 +151,33 @@ class ReactiveRedisSessionRepositoryTests {
.isEqualTo(newSession.getLastAccessedTime().toEpochMilli());
}
@Test
void saveCustomNegativeMaxInactiveIntervalNewSession() {
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.persist(anyString())).willReturn(Mono.just(true));
MapSession mapSession = new MapSession();
mapSession.setMaxInactiveInterval(Duration.ofSeconds(-1));
RedisSession newSession = this.repository.new RedisSession(mapSession, true);
StepVerifier.create(this.repository.save(newSession)).verifyComplete();
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).persist(anyString());
verifyNoMoreInteractions(this.redisOperations);
verifyNoMoreInteractions(this.hashOperations);
Map<String, Object> delta = this.delta.getAllValues().get(0);
assertThat(delta.size()).isEqualTo(3);
assertThat(delta.get(RedisSessionMapper.CREATION_TIME_KEY))
.isEqualTo(newSession.getCreationTime().toEpochMilli());
assertThat(delta.get(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY))
.isEqualTo((int) newSession.getMaxInactiveInterval().getSeconds());
assertThat(delta.get(RedisSessionMapper.LAST_ACCESSED_TIME_KEY))
.isEqualTo(newSession.getLastAccessedTime().toEpochMilli());
}
@Test
void saveSessionNothingChanged() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));

View File

@@ -0,0 +1,9 @@
name: session
title: Spring Session
version: ~
display_version: 2.6
start_page: ROOT:index.adoc
nav:
- modules/ROOT/nav.adoc

View File

@@ -0,0 +1,63 @@
/*
* 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 docs;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* @author Rob Winch
*
*/
class FindByIndexNameSessionRepositoryTests {
@Mock
FindByIndexNameSessionRepository<Session> sessionRepository;
@Mock
Session session;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
void setUsername() {
// tag::set-username[]
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::set-username[]
}
@Test
@SuppressWarnings("unused")
void findByUsername() {
// tag::findby-username[]
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
// end::findby-username[]
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 docs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
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.mockito.Mockito.mock;
/**
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
public class HttpSessionConfigurationNoOpConfigureRedisActionXmlTests {
@Autowired
SessionRepositoryFilter<? extends Session> filter;
@Test
void redisConnectionFactoryNotUsedSinceNoValidation() {
assertThat(this.filter).isNotNull();
}
static RedisConnectionFactory connectionFactory() {
return mock(RedisConnectionFactory.class);
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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 docs;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = this.repository.createSession(); // <2>
// <3>
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); // <4>
S session = this.repository.findById(toSave.getId()); // <5>
// <6>
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = this.repository.createSession(); // <2>
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); // <3>
this.repository.save(toSave); // <4>
S session = this.repository.findById(toSave.getId()); // <5>
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
@SuppressWarnings("unused")
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer()).build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
@SuppressWarnings("unused")
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newHazelcastIndexedSessionRepository() {
// tag::new-hazelcastindexedsessionrepository[]
Config config = new Config();
// ... configure Hazelcast ...
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
// end::new-hazelcastindexedsessionrepository[]
}
@Test
void runSpringHttpSessionConfig() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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 docs;
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.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
@Test
void redisConnectionFactoryNotUsedSinceNoValidation() {
}
@EnableRedisHttpSession
@Configuration
static class Config {
// tag::configure-redis-action[]
@Bean
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// end::configure-redis-action[]
@Bean
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
// tag::class[]
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,36 @@
/*
* 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 docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
// tag::class[]
@EnableSpringWebSession
public class SpringWebSessionConfig {
@Bean
public ReactiveSessionRepository reactiveSessionRepository() {
return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,94 @@
/*
* 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 docs.http;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
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.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
* @author Mark Paluch
* @since 1.2
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
public abstract class AbstractHttpSessionListenerTests {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
SecuritySessionDestroyedListener listener;
@Test
void springSessionDestroyedTranslatedToSpringSecurityDestroyed() {
Session session = new MapSession();
this.publisher.publishEvent(new org.springframework.session.events.SessionDestroyedEvent(this, session));
assertThat(this.listener.getEvent().getId()).isEqualTo(session.getId());
}
static RedisConnectionFactory createMockRedisConnection() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
return factory;
}
static class SecuritySessionDestroyedListener implements ApplicationListener<SessionDestroyedEvent> {
private SessionDestroyedEvent event;
/*
* (non-Javadoc)
*
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.
* springframework.context.ApplicationEvent)
*/
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
this.event = event;
}
SessionDestroyedEvent getEvent() {
return this.event;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 docs.http;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
import org.springframework.session.hazelcast.PrincipalNameExtractor;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
//tag::config[]
@EnableHazelcastHttpSession // <1>
@Configuration
public class HazelcastHttpSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
return Hazelcast.newHazelcastInstance(config); // <4>
}
}
// end::config[]

View File

@@ -0,0 +1,46 @@
/*
* 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 docs.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Rob Winch
*
*/
@ContextConfiguration(classes = { HttpSessionListenerJavaConfigTests.MockConfig.class, RedisHttpSessionConfig.class })
class HttpSessionListenerJavaConfigTests extends AbstractHttpSessionListenerTests {
@Configuration
static class MockConfig {
@Bean
static RedisConnectionFactory redisConnectionFactory() {
return AbstractHttpSessionListenerTests.createMockRedisConnection();
}
@Bean
SecuritySessionDestroyedListener securitySessionDestroyedListener() {
return new SecuritySessionDestroyedListener();
}
}
}

View File

@@ -14,8 +14,15 @@
* limitations under the License.
*/
package docs;
package docs.http;
public class Docs {
import org.springframework.test.context.ContextConfiguration;
/**
* @author Rob Winch
*
*/
@ContextConfiguration
class HttpSessionListenerXmlTests extends AbstractHttpSessionListenerTests {
}

View File

@@ -0,0 +1,37 @@
/*
* 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 docs.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
// tag::config[]
@Configuration
@EnableRedisHttpSession
public class RedisHttpSessionConfig {
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
// ...
}
// end::config[]

View File

@@ -0,0 +1,82 @@
/*
* 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 docs.security;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
/**
* @author rwinch
*/
@EnableWebSecurity
@EnableSpringHttpSession
public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapter {
// @formatter:off
// tag::http-rememberme[]
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ... additional configuration ...
.rememberMe((rememberMe) -> rememberMe
.rememberMeServices(rememberMeServices())
);
// end::http-rememberme[]
http
.formLogin(Customizer.withDefaults())
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
);
}
// tag::rememberme-bean[]
@Bean
public SpringSessionRememberMeServices rememberMeServices() {
SpringSessionRememberMeServices rememberMeServices =
new SpringSessionRememberMeServices();
// optionally customize
rememberMeServices.setAlwaysRemember(true);
return rememberMeServices;
}
// end::rememberme-bean[]
// @formatter:on
@Override
@Bean
public InMemoryUserDetailsManager userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("user").password("{noop}password").roles("USER").build());
}
@Bean
MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,92 @@
/*
* 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 docs.security;
import java.time.Duration;
import java.util.Base64;
import javax.servlet.http.Cookie;
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.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
/**
* @author rwinch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RememberMeSecurityConfiguration.class)
@WebAppConfiguration
@SuppressWarnings("rawtypes")
class RememberMeSecurityConfigurationTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
SessionRepositoryFilter springSessionRepositoryFilter;
@Autowired
SessionRepository<T> sessions;
private MockMvc mockMvc;
@BeforeEach
void setup() {
// @formatter:off
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(this.springSessionRepositoryFilter)
.apply(springSecurity())
.build();
// @formatter:on
}
@Test
void authenticateWhenSpringSessionRememberMeEnabledThenCookieMaxAgeAndSessionExpirationSet() throws Exception {
// @formatter:off
MvcResult result = this.mockMvc
.perform(formLogin())
.andReturn();
// @formatter:on
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofDays(30));
}
}
// end::class[]

View File

@@ -0,0 +1,92 @@
/*
* 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 docs.security;
import java.time.Duration;
import java.util.Base64;
import javax.servlet.http.Cookie;
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.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
/**
* @author rwinch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
@SuppressWarnings("rawtypes")
class RememberMeSecurityConfigurationXmlTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
SessionRepositoryFilter springSessionRepositoryFilter;
@Autowired
SessionRepository<T> sessions;
private MockMvc mockMvc;
@BeforeEach
void setup() {
// @formatter:off
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(this.springSessionRepositoryFilter)
.apply(springSecurity())
.build();
// @formatter:on
}
@Test
void authenticateWhenSpringSessionRememberMeEnabledThenCookieMaxAgeAndSessionExpirationSet() throws Exception {
// @formatter:off
MvcResult result = this.mockMvc
.perform(formLogin())
.andReturn();
// @formatter:on
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofDays(30));
}
}
// end::class[]

View File

@@ -0,0 +1,56 @@
/*
* 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 docs.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
/**
* @author Joris Kuipers
*/
// tag::class[]
@Configuration
public class SecurityConfiguration<S extends Session> extends WebSecurityConfigurerAdapter {
@Autowired
private FindByIndexNameSessionRepository<S> sessionRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
// other config goes here...
.sessionManagement((sessionManagement) -> sessionManagement
.maximumSessions(2)
.sessionRegistry(sessionRegistry())
);
// @formatter:on
}
@Bean
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
}
// end::class[]

View File

@@ -0,0 +1,47 @@
/*
* 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 docs.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @author Rob Winch
*/
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messages").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
}
// end::class[]

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
<!-- tag::configure-redis-action[] -->
<util:constant
static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
<!-- end::configure-redis-action[] -->
<bean class="docs.HttpSessionConfigurationNoOpConfigureRedisActionXmlTests"
factory-method="connectionFactory"/>
</beans>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
<!-- tag::config[] -->
<bean class="org.springframework.security.web.session.HttpSessionEventPublisher"/>
<!-- end::config[] -->
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
<bean class="docs.http.AbstractHttpSessionListenerTests"
factory-method="createMockRedisConnection"/>
<bean class="docs.http.AbstractHttpSessionListenerTests$SecuritySessionDestroyedListener"/>
</beans>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- tag::config[] -->
<security:http>
<!-- ... -->
<security:form-login />
<security:remember-me services-ref="rememberMeServices"/>
</security:http>
<bean id="rememberMeServices"
class="org.springframework.session.security.web.authentication.SpringSessionRememberMeServices"
p:alwaysRemember="true"/>
<!-- end::config[] -->
<security:user-service>
<security:user name="user" password="{noop}password" authorities="ROLE_USER"/>
</security:user-service>
<bean class="org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration"/>
<bean id="springSessionRepository" class="org.springframework.session.MapSessionRepository">
<constructor-arg>
<bean class="java.util.concurrent.ConcurrentHashMap"/>
</constructor-arg>
</bean>
</beans>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
<!-- tag::config[] -->
<security:http>
<!-- other config goes here... -->
<security:session-management>
<security:concurrency-control max-sessions="2" session-registry-ref="sessionRegistry"/>
</security:session-management>
</security:http>
<bean id="sessionRegistry"
class="org.springframework.session.security.SpringSessionBackedSessionRegistry">
<constructor-arg ref="sessionRepository"/>
</bean>
<!-- end::config[] -->
</beans>

View File

@@ -0,0 +1 @@
../../../../spring-session-jdbc/src/main/resources

View File

@@ -0,0 +1 @@
../../../../spring-session-samples

View File

@@ -0,0 +1,24 @@
* xref:whats-new.adoc[What's New]
* xref:samples.adoc[Samples & Guides (Start Here)]
** Boot Samples
*** HttpSession
**** Redis
***** {gh-samples-url}spring-session-sample-boot-redis-json[JSON serialization]
***** {gh-samples-url}spring-session-sample-boot-redis-simple[Simple Redis]
***** xref:guides/boot-redis.adoc[Redis with Events]
**** xref:guides/boot-jdbc.adoc[JDBC]
**** {gh-samples-url}spring-session-sample-boot-hazelcast[HttpSession with Hazelcast]
*** xref:guides/boot-findbyusername.adoc[Find by Username]
*** xref:guides/boot-websocket.adoc[WebSockets]
** WebFlux
*** {gh-samples-url}spring-session-sample-boot-webflux[Redis]
*** xref:guides/boot-webflux-custom-cookie.adoc[Custom Cookie]
** Java Configuration
** XML Configuration
* xref:modules.adoc[Modules]
* xref:http-session.adoc[HttpSession Integration]
* xref:web-socket.adoc[WebSocket Integration]
* xref:web-session.adoc[WebSession Integration]
* xref:spring-security.adoc[Spring Security Integration]
* xref:api.adoc[API Documentation]
* xref:upgrading.adoc[Upgrading]

View File

@@ -1,6 +1,5 @@
= Spring Session - find by username
Rob Winch
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - Spring Boot
Rob Winch, Vedran Pavić
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - Spring Boot
Rob Winch, Vedran Pavić
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - WebFlux with Custom Cookie
Eleftheria Stein-Kousathana
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - WebSocket
Rob Winch
:toc: left
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
:stylesdir: ../
:highlightjsdir: ../js/highlight

View File

@@ -1,6 +1,5 @@
= Spring Session - Custom Cookie
Rob Winch; Eleftheria Stein-Kousathana
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session and Spring Security with Hazelcast
Tommy Ludwig; Rob Winch
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides
@@ -111,7 +110,7 @@ with the same `SerializerConfiguration` of members.
== Servlet Container Initialization
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
Our xref:guides/java-security.adoc#security-spring-configuration[Spring Configuration] created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, Spring needs to load our `SessionConfig` class.

View File

@@ -1,6 +1,5 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch, Vedran Pavić
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch
:toc: left
:version-snapshot: true
:stylesdir: ../
:highlightjsdir: ../js/highlight

View File

@@ -1,6 +1,5 @@
= Spring Session - REST
Rob Winch
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session and Spring Security
Rob Winch
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch, Vedran Pavić
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -1,6 +1,5 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch
:toc: left
:stylesdir: ../
:highlightjsdir: ../js/highlight
:docinfodir: guides

View File

@@ -0,0 +1,194 @@
[[httpsession]]
= `HttpSession` Integration
Spring Session provides transparent integration with `HttpSession`.
This means that developers can switch the `HttpSession` implementation out with an implementation that is backed by Spring Session.
[[httpsession-why]]
== Why Spring Session and `HttpSession`?
We have already mentioned that Spring Session provides transparent integration with `HttpSession`, but what benefits do we get out of this?
* *Clustered Sessions*: Spring Session makes it trivial to support <<httpsession-redis,clustered sessions>> without being tied to an application container specific solution.
* *RESTful APIs*: Spring Session lets providing session IDs in headers work with <<httpsession-rest,RESTful APIs>>
[[httpsession-redis]]
== `HttpSession` with Redis
Using Spring Session with `HttpSession` is enabled by adding a Servlet Filter before anything that uses the `HttpSession`.
You can choose from enabling this by using either:
* <<httpsession-redis-jc,Java-based Configuration>>
* <<httpsession-redis-xml,XML-based Configuration>>
[[httpsession-redis-jc]]
=== Redis Java-based Configuration
This section describes how to use Redis to back `HttpSession` by using Java based configuration.
NOTE: The xref:samples.adoc#samples[ HttpSession Sample] provides a working sample of how to integrate Spring Session and `HttpSession` by using Java configuration.
You can read the basic steps for integration in the next few sections, but we encourage you to follow along with the detailed HttpSession Guide when integrating with your own application.
include::guides/java-redis.adoc[tags=config,leveloffset=+2]
[[httpsession-redis-xml]]
=== Redis XML-based Configuration
This section describes how to use Redis to back `HttpSession` by using XML based configuration.
NOTE: The xref:samples.adoc#samples[ HttpSession XML Sample] provides a working sample of how to integrate Spring Session and `HttpSession` using XML configuration.
You can read the basic steps for integration in the next few sections, but we encourage you to follow along with the detailed HttpSession XML Guide when integrating with your own application.
include::guides/xml-redis.adoc[tags=config,leveloffset=+2]
[[httpsession-jdbc]]
== `HttpSession` with JDBC
You can use Spring Session with `HttpSession` by adding a servlet filter before anything that uses the `HttpSession`.
You can choose to do in any of the following ways:
* <<httpsession-jdbc-jc,Java-based Configuration>>
* <<httpsession-jdbc-xml,XML-based Configuration>>
* <<httpsession-jdbc-boot,Spring Boot-based Configuration>>
[[httpsession-jdbc-jc]]
=== JDBC Java-based Configuration
This section describes how to use a relational database to back `HttpSession` when you use Java-based configuration.
NOTE: The xref:samples.adoc#samples[ HttpSession JDBC Sample] provides a working sample of how to integrate Spring Session and `HttpSession` by using Java configuration.
You can read the basic steps for integration in the next few sections, but we encouraged you to follow along with the detailed HttpSession JDBC Guide when integrating with your own application.
include::guides/java-jdbc.adoc[tags=config,leveloffset=+2]
[[httpsession-jdbc-xml]]
=== JDBC XML-based Configuration
This section describes how to use a relational database to back `HttpSession` when you use XML based configuration.
NOTE: The xref:samples.adoc#samples[ HttpSession JDBC XML Sample] provides a working sample of how to integrate Spring Session and `HttpSession` by using XML configuration.
You can read the basic steps for integration in the next few sections, but we encourage you to follow along with the detailed HttpSession JDBC XML Guide when integrating with your own application.
include::guides/xml-jdbc.adoc[tags=config,leveloffset=+2]
[[httpsession-jdbc-boot]]
=== JDBC Spring Boot-based Configuration
This section describes how to use a relational database to back `HttpSession` when you use Spring Boot.
NOTE: The xref:samples.adoc#samples[ HttpSession JDBC Spring Boot Sample] provides a working sample of how to integrate Spring Session and `HttpSession` by using Spring Boot.
You can read the basic steps for integration in the next few sections, but we encourage you to follow along with the detailed HttpSession JDBC Spring Boot Guide when integrating with your own application.
include::guides/boot-jdbc.adoc[tags=config,leveloffset=+2]
[[httpsession-hazelcast]]
== HttpSession with Hazelcast
Using Spring Session with `HttpSession` is enabled by adding a Servlet Filter before anything that uses the `HttpSession`.
This section describes how to use Hazelcast to back `HttpSession` by using Java-based configuration.
NOTE: The xref:samples.adoc#samples[ Hazelcast Spring Sample] provides a working sample of how to integrate Spring Session and `HttpSession` by using Java configuration.
You can read the basic steps for integration in the next few sections, but we encourage you to follow along with the detailed Hazelcast Spring Guide when integrating with your own application.
include::guides/java-hazelcast.adoc[tags=config,leveloffset=+1]
[[httpsession-how]]
== How `HttpSession` Integration Works
Fortunately, both `HttpSession` and `HttpServletRequest` (the API for obtaining an `HttpSession`) are both interfaces.
This means that we can provide our own implementations for each of these APIs.
NOTE: This section describes how Spring Session provides transparent integration with `HttpSession`. We offer this content so that you can understand what is happening under the covers. This functionality is already integrated and you do NOT need to implement this logic yourself.
First, we create a custom `HttpServletRequest` that returns a custom implementation of `HttpSession`.
It looks something like the following:
====
[source, java]
----
public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
public SessionRepositoryRequestWrapper(HttpServletRequest original) {
super(original);
}
public HttpSession getSession() {
return getSession(true);
}
public HttpSession getSession(boolean createNew) {
// create an HttpSession implementation from Spring Session
}
// ... other methods delegate to the original HttpServletRequest ...
}
----
====
Any method that returns an `HttpSession` is overridden.
All other methods are implemented by `HttpServletRequestWrapper` and delegate to the original `HttpServletRequest` implementation.
We replace the `HttpServletRequest` implementation by using a servlet `Filter` called `SessionRepositoryFilter`.
The following pseudocode shows how it works:
====
[source, java]
----
public class SessionRepositoryFilter implements Filter {
public doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
SessionRepositoryRequestWrapper customRequest =
new SessionRepositoryRequestWrapper(httpRequest);
chain.doFilter(customRequest, response, chain);
}
// ...
}
----
====
By passing a custom `HttpServletRequest` implementation into the `FilterChain`, we ensure that anything invoked after our `Filter` uses the custom `HttpSession` implementation.
This highlights why it is important that Spring Session's `SessionRepositoryFilter` be placed before anything that interacts with the `HttpSession`.
[[httpsession-rest]]
== `HttpSession` and RESTful APIs
Spring Session can work with RESTful APIs by letting the session be provided in a header.
NOTE: The xref:samples.adoc#samples[ REST Sample] provides a working sample of how to use Spring Session in a REST application to support authenticating with a header.
You can follow the basic steps for integration described in the next few sections, but we encourage you to follow along with the detailed REST Guide when integrating with your own application.
include::guides/java-rest.adoc[tags=config,leveloffset=+1]
[[httpsession-httpsessionlistener]]
== Using `HttpSessionListener`
Spring Session supports `HttpSessionListener` by translating `SessionDestroyedEvent` and `SessionCreatedEvent` into `HttpSessionEvent` by declaring `SessionEventHttpSessionListenerAdapter`.
To use this support, you need to:
* Ensure your `SessionRepository` implementation supports and is configured to fire `SessionDestroyedEvent` and `SessionCreatedEvent`.
* Configure `SessionEventHttpSessionListenerAdapter` as a Spring bean.
* Inject every `HttpSessionListener` into the `SessionEventHttpSessionListenerAdapter`
If you use the configuration support documented in <<httpsession-redis,`HttpSession` with Redis>>, all you need to do is register every `HttpSessionListener` as a bean.
For example, assume you want to support Spring Security's concurrency control and need to use `HttpSessionEventPublisher`. In that case, you can add `HttpSessionEventPublisher` as a bean.
In Java configuration, this might look like the following:
====
[source,java,indent=0]
----
include::{docs-test-dir}docs/http/RedisHttpSessionConfig.java[tags=config]
----
====
In XML configuration, this might look like the following:
====
[source,xml,indent=0]
----
include::{docs-test-resources-dir}docs/http/HttpSessionListenerXmlTests-context.xml[tags=config]
----
====

View File

@@ -0,0 +1,74 @@
= Spring Session
Rob Winch; Vedran Pavić; Jay Bryant; Eleftheria Stein-Kousathana
:doctype: book
:indexdoc-tests: {docs-test-dir}docs/IndexDocTests.java
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
[[abstract]]
Spring Session provides an API and implementations for managing a user's session information.
[[introduction]]
Spring Session provides an API and implementations for managing a user's session information while also making it trivial to support clustered sessions without being tied to an application container-specific solution.
It also provides transparent integration with:
* xref:http-session.adoc#httpsession[HttpSession]: Allows replacing the `HttpSession` in an application container-neutral way, with support for providing session IDs in headers to work with RESTful APIs.
* xref:web-socket.adoc#websocket[WebSocket]: Provides the ability to keep the `HttpSession` alive when receiving WebSocket messages
* xref:web-session.adoc#websession[WebSession]: Allows replacing the Spring WebFlux's `WebSession` in an application container-neutral way.
[[community]]
== Spring Session Community
We are glad to consider you a part of our community.
The following sections provide additional about how to interact with the Spring Session community.
[[community-support]]
=== Support
You can get help by asking questions on https://stackoverflow.com/questions/tagged/spring-session[Stack Overflow with the `spring-session` tag].
Similarly, we encourage helping others by answering questions on Stack Overflow.
[[community-source]]
=== Source Code
You can find the source code on GitHub at https://github.com/spring-projects/spring-session/
[[community-issues]]
=== Issue Tracking
We track issues in GitHub issues at https://github.com/spring-projects/spring-session/issues
[[community-contributing]]
=== Contributing
We appreciate https://help.github.com/articles/using-pull-requests/[pull requests].
[[community-license]]
=== License
Spring Session is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 license].
[[community-extensions]]
=== Community Extensions
|===
| Name | Location
| Spring Session Infinispan
| https://infinispan.org/infinispan-spring-boot/master/spring_boot_starter.html#_enabling_spring_session_support
|===
[[minimum-requirements]]
== Minimum Requirements
The minimum requirements for Spring Session are:
* Java 8+.
* If you run in a Servlet Container (not required), Servlet 3.1+.
* If you use other Spring libraries (not required), the minimum required version is Spring 5.0.x.
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support xref:api.adoc#api-redisindexedsessionrepository-expiration[Session Expiration]
* `@EnableHazelcastHttpSession` requires Hazelcast 3.6+. This is necessary to support xref:api.adoc#api-enablehazelcasthttpsession-storage[`FindByIndexNameSessionRepository`]
NOTE: At its core, Spring Session has a required dependency only on `spring-jcl`.
For an example of using Spring Session without any other Spring dependencies, see the xref:samples.adoc#samples[hazelcast sample] application.

View File

@@ -0,0 +1,23 @@
[[modules]]
= Spring Session Modules
In Spring Session 1.x, all of the Spring Session's `SessionRepository` implementations were available within the `spring-session` artifact.
While convenient, this approach was not sustainable long-term as more features and `SessionRepository` implementations were added to the project.
Starting with Spring Session 2.0, the project has been split into Spring Session Core module and several other modules that carry `SessionRepository` implementations and functionality related to the specific data store.
Users of Spring Data should find this arrangement familiar, with Spring Session Core module taking a role equivalent to Spring Data Commons and providing core functionalities and APIs, with other modules containing data store specific implementations.
As part of this split, the Spring Session Data MongoDB and Spring Session Data GemFire modules were moved to separate repositories.
Now the situation with project's repositories/modules is as follows:
* https://github.com/spring-projects/spring-session[`spring-session` repository]
** Hosts the Spring Session Core, Spring Session Data Redis, Spring Session JDBC, and Spring Session Hazelcast modules
* https://github.com/spring-projects/spring-session-data-mongodb[`spring-session-data-mongodb` repository]
** Hosts the Spring Session Data MongoDB module. Spring Session Data MongoDB has its own user guide, which you can find at the [https://spring.io/projects/spring-session-data-mongodb#learnSpring site].
* https://github.com/spring-projects/spring-session-data-geode[`spring-session-data-geode` repository]
** Hosts the Spring Session Data Geode modules. Spring Session Data Geode has its own user guide, which you can find at the [https://spring.io/projects/spring-session-data-geode#learn site].
Finally, Spring Session now also provides a Maven BOM ("`bill of materials`") module in order to help users with version management concerns:
* https://github.com/spring-projects/spring-session-bom[`spring-session-bom` repository]
** Hosts the Spring Session BOM module

View File

@@ -0,0 +1,100 @@
[[samples]]
= Samples and Guides (Start Here)
To get started with Spring Session, the best place to start is our Sample Applications.
.Sample Applications that use Spring Boot
|===
| Source | Description | Guide
| {gh-samples-url}spring-session-sample-boot-redis[HttpSession with Redis]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis.
| link:guides/boot-redis.html[HttpSession with Redis Guide]
| {gh-samples-url}spring-session-sample-boot-jdbc[HttpSession with JDBC]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store.
| link:guides/boot-jdbc.html[HttpSession with JDBC Guide]
| {gh-samples-url}spring-session-sample-boot-hazelcast[HttpSession with Hazelcast]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Hazelcast.
|
| {gh-samples-url}spring-session-sample-boot-findbyusername[Find by Username]
| Demonstrates how to use Spring Session to find sessions by username.
| link:guides/boot-findbyusername.html[Find by Username Guide]
| {gh-samples-url}spring-session-sample-boot-websocket[WebSockets]
| Demonstrates how to use Spring Session with WebSockets.
| link:guides/boot-websocket.html[WebSockets Guide]
| {gh-samples-url}spring-session-sample-boot-webflux[WebFlux]
| Demonstrates how to use Spring Session to replace the Spring WebFlux's `WebSession` with Redis.
|
| {gh-samples-url}spring-session-sample-boot-webflux-custom-cookie[WebFlux with Custom Cookie]
| Demonstrates how to use Spring Session to customize the Session cookie in a WebFlux based application.
| link:guides/boot-webflux-custom-cookie.html[WebFlux with Custom Cookie Guide]
| {gh-samples-url}spring-session-sample-boot-redis-json[HttpSession with Redis JSON serialization]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using JSON serialization.
|
| {gh-samples-url}spring-session-sample-boot-redis-simple[HttpSession with simple Redis `SessionRepository`]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `RedisSessionRepository`.
|
|===
.Sample Applications that use Spring Java-based configuration
|===
| Source | Description | Guide
| {gh-samples-url}spring-session-sample-javaconfig-redis[HttpSession with Redis]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis.
| link:guides/java-redis.html[HttpSession with Redis Guide]
| {gh-samples-url}spring-session-sample-javaconfig-jdbc[HttpSession with JDBC]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store.
| link:guides/java-jdbc.html[HttpSession with JDBC Guide]
| {gh-samples-url}spring-session-sample-javaconfig-hazelcast[HttpSession with Hazelcast]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Hazelcast.
| link:guides/java-hazelcast.html[HttpSession with Hazelcast Guide]
| {gh-samples-url}spring-session-sample-javaconfig-custom-cookie[Custom Cookie]
| Demonstrates how to use Spring Session and customize the cookie.
| link:guides/java-custom-cookie.html[Custom Cookie Guide]
| {gh-samples-url}spring-session-sample-javaconfig-security[Spring Security]
| Demonstrates how to use Spring Session with an existing Spring Security application.
| link:guides/java-security.html[Spring Security Guide]
| {gh-samples-url}spring-session-sample-javaconfig-rest[REST]
| Demonstrates how to use Spring Session in a REST application to support authenticating with a header.
| link:guides/java-rest.html[REST Guide]
|===
.Sample Applications that use Spring XML-based configuration
|===
| Source | Description | Guide
| {gh-samples-url}spring-session-sample-xml-redis[HttpSession with Redis]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a Redis store.
| link:guides/xml-redis.html[HttpSession with Redis Guide]
| {gh-samples-url}spring-session-sample-xml-jdbc[HttpSession with JDBC]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store.
| link:guides/xml-jdbc.html[HttpSession with JDBC Guide]
|===
.Miscellaneous sample Applications
|===
| Source | Description | Guide
| {gh-samples-url}spring-session-sample-misc-hazelcast[Hazelcast]
| Demonstrates how to use Spring Session with Hazelcast in a Java EE application.
|
|===

View File

@@ -0,0 +1,77 @@
[[spring-security]]
= Spring Security Integration
Spring Session provides integration with Spring Security.
[[spring-security-rememberme]]
== Spring Security Remember-me Support
Spring Session provides integration with https://docs.spring.io/spring-security/site/docs/{spring-security-version}/reference/html5/#servlet-rememberme[Spring Security's Remember-me Authentication].
The support:
* Changes the session expiration length
* Ensures that the session cookie expires at `Integer.MAX_VALUE`.
The cookie expiration is set to the largest possible value, because the cookie is set only when the session is created.
If it were set to the same value as the session expiration, the session would get renewed when the user used it but the cookie expiration would not be updated (causing the expiration to be fixed).
To configure Spring Session with Spring Security in Java Configuration, you can use the following listing as a guide:
====
[source,java,indent=0]
----
include::{docs-test-dir}docs/security/RememberMeSecurityConfiguration.java[tags=http-rememberme]
}
include::{docs-test-dir}docs/security/RememberMeSecurityConfiguration.java[tags=rememberme-bean]
----
====
An XML-based configuration would look something like the following:
====
[source,xml,indent=0]
----
include::{docs-test-resources-dir}docs/security/RememberMeSecurityConfigurationXmlTests-context.xml[tags=config]
----
====
[[spring-security-concurrent-sessions]]
== Spring Security Concurrent Session Control
Spring Session provides integration with Spring Security to support its concurrent session control.
This allows limiting the number of active sessions that a single user can have concurrently, but, unlike the default
Spring Security support, this also works in a clustered environment. This is done by providing a custom
implementation of Spring Security's `SessionRegistry` interface.
When using Spring Security's Java config DSL, you can configure the custom `SessionRegistry` through the
`SessionManagementConfigurer`, as the following listing shows:
====
[source,java,indent=0]
----
include::{docs-test-dir}docs/security/SecurityConfiguration.java[tags=class]
----
====
This assumes that you have also configured Spring Session to provide a `FindByIndexNameSessionRepository` that
returns `Session` instances.
When using XML configuration, it would look something like the following listing:
====
[source,xml,indent=0]
----
include::{docs-test-resources-dir}docs/security/security-config.xml[tags=config]
----
====
This assumes that your Spring Session `SessionRegistry` bean is called `sessionRegistry`, which is the name used by all
`SpringHttpSessionConfiguration` subclasses.
[[spring-security-concurrent-sessions-limitations]]
== Limitations
Spring Session's implementation of Spring Security's `SessionRegistry` interface does not support the `getAllPrincipals`
method, as this information cannot be retrieved by using Spring Session. This method is never called by Spring Security,
so this affects only applications that access the `SessionRegistry` themselves.

View File

@@ -0,0 +1,50 @@
[[upgrading-2.0]]
= Upgrading to 2.x
With the new major release version, the Spring Session team took the opportunity to make some non-passive changes.
The focus of these changes is to improve and harmonize Spring Session's APIs as well as remove the deprecated components.
== Baseline Update
Spring Session 2.0 requires Java 8 and Spring Framework 5.0 as a baseline, since its entire codebase is now based on Java 8 source code.
See https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x[Upgrading to Spring Framework 5.x] for more on upgrading Spring Framework.
== Replaced and Removed Modules
As a part of the project's splitting of the modules, the existing `spring-session` has been replaced with the `spring-session-core` module.
The `spring-session-core` module holds only the common set of APIs and components, while other modules contain the implementation of the appropriate `SessionRepository` and functionality related to that data store.
This applies to several existing modules that were previously a simple dependency aggregator helper module.
With new module arrangement, the following modules actually carry the implementation:
* Spring Session Data Redis
* Spring Session JDBC
* Spring Session Hazelcast
Also, the following modules were removed from the main project repository:
* Spring Session Data MongoDB
* Spring Session Data GemFire
Note that these two have moved to separate repositories and continue to be available under new artifact names:
* https://github.com/spring-projects/spring-session-data-mongodb[`spring-session-data-mongodb`]
* https://github.com/spring-projects/spring-session-data-geode[`spring-session-data-geode`]
== Replaced and Removed Packages, Classes, and Methods
The following changes were made to packages, classes, and methods:
* `ExpiringSession` API has been merged into the `Session` API.
* The `Session` API has been enhanced to make full use of Java 8.
* The `Session` API has been extended with `changeSessionId` support.
* The `SessionRepository` API has been updated to better align with Spring Data method naming conventions.
* `AbstractSessionEvent` and its subclasses are no longer constructable without an underlying `Session` object.
* The Redis namespace used by `RedisOperationsSessionRepository` is now fully configurable, instead of being partially configurable.
* Redis configuration support has been updated to avoid registering a Spring Session-specific `RedisTemplate` bean.
* JDBC configuration support has been updated to avoid registering a Spring Session-specific `JdbcTemplate` bean.
* Previously deprecated classes and methods have been removed across the codebase
== Dropped Support
As a part of the changes to `HttpSessionStrategy` and its alignment to the counterpart from the reactive world, the support for managing multiple users' sessions in a single browser instance has been removed.
The introduction of a new API to replace this functionality is under consideration for future releases.

View File

@@ -0,0 +1,116 @@
[[websession]]
= WebSession Integration
Spring Session provides transparent integration with Spring WebFlux's `WebSession`.
This means that you can switch the `WebSession` implementation out with an implementation that is backed by Spring Session.
[[websession-why]]
== Why Spring Session and WebSession?
We have already mentioned that Spring Session provides transparent integration with Spring WebFlux's `WebSession`, but what benefits do we get out of this?
As with `HttpSession`, Spring Session makes it trivial to support <<websession-redis,clustered sessions>> without being tied to an application container specific solution.
[[websession-redis]]
== WebSession with Redis
Using Spring Session with `WebSession` is enabled by registering a `WebSessionManager` implementation backed by Spring Session's `ReactiveSessionRepository`.
The Spring configuration is responsible for creating a `WebSessionManager` that replaces the `WebSession` implementation with an implementation backed by Spring Session.
To do so, add the following Spring Configuration:
====
[source, java]
----
@EnableRedisWebSession // <1>
public class SessionConfiguration {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(); // <2>
}
}
----
<1> The `@EnableRedisWebSession` annotation creates a Spring bean with the name of `webSessionManager`. That bean implements the `WebSessionManager`.
This is what is in charge of replacing the `WebSession` implementation to be backed by Spring Session.
In this instance, Spring Session is backed by Redis.
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
We configure the connection to connect to localhost on the default port (6379)
For more information on configuring Spring Data Redis, see the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
====
[[websession-how]]
== How WebSession Integration Works
It is considerably easier for Spring Session to integrate with Spring WebFlux and its `WebSession`, compared to Servlet API and its `HttpSession`.
Spring WebFlux provides the `WebSessionStore` API, which presents a strategy for persisting `WebSession`.
NOTE: This section describes how Spring Session provides transparent integration with `WebSession`. We offer this content so that you can understand what is happening under the covers. This functionality is already integrated and you do NOT need to implement this logic yourself.
First, we create a custom `SpringSessionWebSession` that delegates to Spring Session's `Session`.
It looks something like the following:
====
[source, java]
----
public class SpringSessionWebSession implements WebSession {
enum State {
NEW, STARTED
}
private final S session;
private AtomicReference<State> state = new AtomicReference<>();
SpringSessionWebSession(S session, State state) {
this.session = session;
this.state.set(state);
}
@Override
public void start() {
this.state.compareAndSet(State.NEW, State.STARTED);
}
@Override
public boolean isStarted() {
State value = this.state.get();
return (State.STARTED.equals(value)
|| (State.NEW.equals(value) && !this.session.getAttributes().isEmpty()));
}
@Override
public Mono<Void> changeSessionId() {
return Mono.defer(() -> {
this.session.changeSessionId();
return save();
});
}
// ... other methods delegate to the original Session
}
----
====
Next, we create a custom `WebSessionStore` that delegates to the `ReactiveSessionRepository` and wraps `Session` into custom `WebSession` implementation, as the following listing shows:
====
[source, java]
----
public class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore {
private final ReactiveSessionRepository<S> sessions;
public SpringSessionWebSessionStore(ReactiveSessionRepository<S> reactiveSessionRepository) {
this.sessions = reactiveSessionRepository;
}
// ...
}
----
====
To be detected by Spring WebFlux, this custom `WebSessionStore` needs to be registered with `ApplicationContext` as a bean named `webSessionManager`.
For additional information on Spring WebFlux, see the https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html/web-reactive.html[Spring Framework Reference Documentation].

View File

@@ -0,0 +1,32 @@
[[websocket]]
= WebSocket Integration
Spring Session provides transparent integration with Spring's WebSocket support.
include::guides/boot-websocket.adoc[tags=disclaimer,leveloffset=+1]
[[websocket-why]]
== Why Spring Session and WebSockets?
So why do we need Spring Session when we use WebSockets?
Consider an email application that does much of its work through HTTP requests.
However, there is also a chat application embedded within it that works over WebSocket APIs.
If a user is actively chatting with someone, we should not timeout the `HttpSession`, since this would be a pretty poor user experience.
However, this is exactly what https://java.net/jira/browse/WEBSOCKET_SPEC-175[JSR-356] does.
Another issue is that, according to JSR-356, if the `HttpSession` times out, any WebSocket that was created with that `HttpSession` and an authenticated user should be forcibly closed.
This means that, if we are actively chatting in our application and are not using the HttpSession, we also do disconnect from our conversation.
[[websocket-usage]]
== WebSocket Usage
The xref:samples.adoc#samples[ WebSocket Sample] provides a working sample of how to integrate Spring Session with WebSockets.
You can follow the basic steps for integration described in the next few headings, but we encourage you to follow along with the detailed WebSocket Guide when integrating with your own application.
[[websocket-httpsession]]
=== `HttpSession` Integration
Before using WebSocket integration, you should be sure that you have xref:http-session.adoc#httpsession[`HttpSession` Integration] working first.
include::guides/boot-websocket.adoc[tags=config,leveloffset=+2]

View File

@@ -0,0 +1,4 @@
= What's New
Check also the Spring Session BOM https://github.com/spring-projects/spring-session-bom/wiki#release-notes[release notes]
for a list of new and noteworthy features, as well as upgrade instructions for each release.

View File

@@ -23,8 +23,59 @@ dependencies {
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
sourceSets {
test {
java {
srcDirs = ['modules/ROOT/examples/java']
}
resources {
srcDirs = ['modules/ROOT/examples/resources']
}
}
}
def versions = dependencyManagement.managedVersions
tasks.register("generateAntora") {
group = "Documentation"
description = "Generates the antora.yml for dynamic properties"
doLast {
def dollar = '$'
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects/spring-session/tree/$ghTag"
def outputFile = new File("$buildDir/generateAntora/antora.yml")
outputFile.getParentFile().mkdirs()
outputFile.createNewFile()
outputFile.setText("""name: session
title: Spring Session
version: ~
display_version: 2.6
start_page: ROOT:index.adoc
asciidoc:
attributes:
download-url: "https://github.com/spring-projects/spring-session/archive/${ghTag}.zip"
gh-samples-url: "$ghUrl/spring-session-samples/"
samples-dir: "example${dollar}spring-session-samples/"
session-jdbc-main-resources-dir: "example${dollar}session-jdbc-main-resources-dir/"
docs-test-dir: "example${dollar}java/"
websocketdoc-test-dir: 'example${dollar}java/docs/websocket/'
docs-test-resources-dir: "example${dollar}resources/"
indexdoc-tests: "example${dollar}java/docs/IndexDocTests.java"
spring-session-version: ${project.version}
version-milestone: $milestoneBuild
version-release: $releaseBuild
version-snapshot: $snapshotBuild
spring-boot-version: ${project.springBootVersion}
spring-data-redis-version: ${versions['org.springframework.data:spring-data-redis']}
spring-framework-version: ${versions['org.springframework:spring-core']}
spring-security-version: ${versions['org.springframework.security:spring-security-core']}
hazelcast-version: ${versions['com.hazelcast:hazelcast']}
lettuce-version: ${versions['io.lettuce:lettuce-core']}
""")
}
}
asciidoctorPdf {
clearSources()
sources {

View File

@@ -16,6 +16,9 @@
package org.springframework.session.hazelcast;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Test;
@@ -198,4 +201,51 @@ abstract class AbstractHazelcast4IndexedSessionRepositoryITests {
this.repository.deleteById(sessionId);
}
@Test
void createAndUpdateSessionWhileKeepingOriginalTimeToLiveConfiguredOnRepository() {
final Duration defaultSessionTimeout = Duration.ofSeconds(1800);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
}
@Test
void createAndUpdateSessionWhileKeepingTimeToLiveSetOnSession() {
final Duration individualSessionTimeout = Duration.ofSeconds(23);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(individualSessionTimeout);
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setAttribute("attribute", "value");
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2014-2021 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.hazelcast;
import java.util.Map;
import com.hazelcast.core.HazelcastInstance;
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.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.FlushMode;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
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;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using embedded
* topology, with flush mode set to immediate.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class FlushImmediateHazelcast4IndexedSessionRepositoryITests {
@Autowired
private Hazelcast4IndexedSessionRepository repository;
@Test
void createSessionWithSecurityContextAndFindByPrincipalName() {
String username = "saves-" + System.currentTimeMillis();
Hazelcast4IndexedSessionRepository.HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
this.repository.save(session);
Map<String, Hazelcast4IndexedSessionRepository.HazelcastSession> findByPrincipalName = this.repository
.findByPrincipalName(username);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(sessionId);
this.repository.deleteById(sessionId);
}
@EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE)
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -57,7 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@WebAppConfiguration
class SessionEventHazelcast4IndexedSessionRepositoryTests<S extends Session> {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 2;
@Autowired
private SessionRepository<S> repository;
@@ -199,6 +199,29 @@ class SessionEventHazelcast4IndexedSessionRepositoryTests<S extends Session> {
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Test // gh-1899
void updateSessionAndExpireAfterOriginalTimeToLiveTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
.isInstanceOf(SessionExpiredEvent.class);
// Assert this after the expired event was received because it would otherwise do
// its own expiration check and explicitly delete the session from Hazelcast
// regardless of the TTL of the IMap entry.
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -54,7 +54,6 @@ import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A {@link org.springframework.session.SessionRepository} implementation using Hazelcast
@@ -128,8 +127,6 @@ public class Hazelcast4IndexedSessionRepository
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final boolean SUPPORTS_SET_TTL = ClassUtils.hasAtLeastOneMethodWithName(IMap.class, "setTtl");
private static final Log logger = LogFactory.getLog(Hazelcast4IndexedSessionRepository.class);
private final HazelcastInstance hazelcastInstance;
@@ -262,9 +259,6 @@ public class Hazelcast4IndexedSessionRepository
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
@@ -275,10 +269,6 @@ public class Hazelcast4IndexedSessionRepository
session.clearChangeFlags();
}
private void updateTtl(HazelcastSession session) {
this.sessions.setTtl(session.getId(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
}
@Override
public HazelcastSession findById(String id) {
MapSession saved = this.sessions.get(id);
@@ -416,6 +406,7 @@ public class Hazelcast4IndexedSessionRepository
@Override
public void setMaxInactiveInterval(Duration interval) {
Assert.notNull(interval, "interval must not be null");
this.delegate.setMaxInactiveInterval(interval);
this.maxInactiveIntervalChanged = true;
flushImmediateIfNecessary();
@@ -450,6 +441,7 @@ public class Hazelcast4IndexedSessionRepository
.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
this.delta.put(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -63,13 +63,8 @@ public class Hazelcast4SessionUpdateEntryProcessor implements EntryProcessor<Str
}
}
}
if (this.maxInactiveInterval != null) {
((ExtendedMapEntry<String, MapSession>) entry).setValue(value, this.maxInactiveInterval.getSeconds(),
TimeUnit.SECONDS);
}
else {
entry.setValue(value);
}
((ExtendedMapEntry<String, MapSession>) entry).setValue(value, value.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
return Boolean.TRUE;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -46,7 +46,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
@@ -254,7 +253,6 @@ class Hazelcast4IndexedSessionRepositoryTests {
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verify(this.sessions, times(1)).set(eq(sessionId), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions).setTtl(eq(sessionId), anyLong(), any());
verify(this.sessions, times(1)).executeOnKey(eq(sessionId), any(EntryProcessor.class));
this.repository.save(session);

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2014-2021 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.hazelcast;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import com.hazelcast.map.ExtendedMapEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.session.MapSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class Hazelcast4SessionUpdateEntryProcessorTest {
private Hazelcast4SessionUpdateEntryProcessor processor;
@BeforeEach
void setUp() {
this.processor = new Hazelcast4SessionUpdateEntryProcessor();
}
@Test
void shouldReturnFalseIfNoSessionExistsInHazelcastMapEntry() {
@SuppressWarnings("unchecked")
ExtendedMapEntry<String, MapSession> mapEntry = mock(ExtendedMapEntry.class);
Object result = this.processor.process(mapEntry);
assertThat(result).isEqualTo(Boolean.FALSE);
}
@Test
void shouldUpdateMaxInactiveIntervalOnSessionAndSetMapEntryValueWithNewTimeToLive() {
Duration newMaxInactiveInterval = Duration.ofSeconds(123L);
MapSession mapSession = new MapSession();
@SuppressWarnings("unchecked")
ExtendedMapEntry<String, MapSession> mapEntry = mock(ExtendedMapEntry.class);
given(mapEntry.getValue()).willReturn(mapSession);
this.processor.setMaxInactiveInterval(newMaxInactiveInterval);
Object result = this.processor.process(mapEntry);
assertThat(result).isEqualTo(Boolean.TRUE);
assertThat(mapSession.getMaxInactiveInterval()).isEqualTo(newMaxInactiveInterval);
verify(mapEntry).setValue(mapSession, newMaxInactiveInterval.getSeconds(), TimeUnit.SECONDS);
}
@Test
void shouldSetMapEntryValueWithOldTimeToLiveIfNoChangeToMaxInactiveIntervalIsRegistered() {
Duration maxInactiveInterval = Duration.ofSeconds(123L);
MapSession mapSession = new MapSession();
mapSession.setMaxInactiveInterval(maxInactiveInterval);
@SuppressWarnings("unchecked")
ExtendedMapEntry<String, MapSession> mapEntry = mock(ExtendedMapEntry.class);
given(mapEntry.getValue()).willReturn(mapSession);
Object result = this.processor.process(mapEntry);
assertThat(result).isEqualTo(Boolean.TRUE);
assertThat(mapSession.getMaxInactiveInterval()).isEqualTo(maxInactiveInterval);
verify(mapEntry).setValue(mapSession, maxInactiveInterval.getSeconds(), TimeUnit.SECONDS);
}
@Test
void shouldUpdateLastAccessTimeOnSessionAndSetMapEntryValueWithOldTimeToLive() {
Instant lastAccessTime = Instant.ofEpochSecond(1234L);
MapSession mapSession = new MapSession();
@SuppressWarnings("unchecked")
ExtendedMapEntry<String, MapSession> mapEntry = mock(ExtendedMapEntry.class);
given(mapEntry.getValue()).willReturn(mapSession);
this.processor.setLastAccessedTime(lastAccessTime);
Object result = this.processor.process(mapEntry);
assertThat(result).isEqualTo(Boolean.TRUE);
assertThat(mapSession.getLastAccessedTime()).isEqualTo(lastAccessTime);
verify(mapEntry).setValue(mapSession, mapSession.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
}
@Test
void shouldUpdateSessionAttributesFromDeltaAndSetMapEntryValueWithOldTimeToLive() {
MapSession mapSession = new MapSession();
mapSession.setAttribute("changed", "oldValue");
mapSession.setAttribute("removed", "existingValue");
@SuppressWarnings("unchecked")
ExtendedMapEntry<String, MapSession> mapEntry = mock(ExtendedMapEntry.class);
given(mapEntry.getValue()).willReturn(mapSession);
HashMap<String, Object> delta = new HashMap<>();
delta.put("added", "addedValue");
delta.put("changed", "newValue");
delta.put("removed", null);
this.processor.setDelta(delta);
Object result = this.processor.process(mapEntry);
assertThat(result).isEqualTo(Boolean.TRUE);
assertThat((String) mapSession.getAttribute("added")).isEqualTo("addedValue");
assertThat((String) mapSession.getAttribute("changed")).isEqualTo("newValue");
assertThat((String) mapSession.getAttribute("removed")).isNull();
verify(mapEntry).setValue(mapSession, mapSession.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2014-2021 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.hazelcast;
import java.util.Map;
import com.hazelcast.core.HazelcastInstance;
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.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.FlushMode;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
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;
/**
* Integration tests for {@link HazelcastIndexedSessionRepository} using embedded
* topology, with flush mode set to immediate.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class FlushImmediateHazelcastIndexedSessionRepositoryITests {
@Autowired
private HazelcastIndexedSessionRepository repository;
@Test
void createSessionWithSecurityContextAndFindByPrincipalName() {
String username = "saves-" + System.currentTimeMillis();
HazelcastIndexedSessionRepository.HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
this.repository.save(session);
Map<String, HazelcastIndexedSessionRepository.HazelcastSession> findByPrincipalName = this.repository
.findByPrincipalName(username);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(sessionId);
this.repository.deleteById(sessionId);
}
@EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE)
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -58,7 +58,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@WebAppConfiguration
class SessionEventHazelcastIndexedSessionRepositoryTests<S extends Session> {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 2;
@Autowired
private SessionRepository<S> repository;

View File

@@ -441,6 +441,7 @@ public class HazelcastIndexedSessionRepository
.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
this.delta.put(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -30,12 +30,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository;
@@ -85,7 +85,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
}
@Bean
public SessionRepository<?> sessionRepository() {
public FindByIndexNameSessionRepository<?> sessionRepository() {
if (hazelcast4) {
return createHazelcast4IndexedSessionRepository();
}

View File

@@ -46,7 +46,7 @@ final class DatabaseContainers {
}
static MySQLContainer<?> mySql() {
return new MySQLContainer<>("mysql:8.0.26");
return new MySQLContainer<>("mysql:8.0.27");
}
static OracleContainer oracle() {
@@ -68,7 +68,7 @@ final class DatabaseContainers {
}
static PostgreSQLContainer<?> postgreSql() {
return new PostgreSQLContainer<>("postgres:13.4");
return new PostgreSQLContainer<>("postgres:14.0");
}
static MSSQLServerContainer<?> sqlServer() {

View File

@@ -1,10 +1,10 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.12.4'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.0'
}
dependencies {
dependency 'ch.qos.logback:logback-classic:1.2.5'
dependency 'ch.qos.logback:logback-classic:1.2.6'
dependency 'com.maxmind.geoip2:geoip2:2.15.0'
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2'
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.3'

View File

@@ -1,19 +1,19 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-session-hazelcast')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast-client"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator-core"
compile project(':spring-session-hazelcast')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator-core"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.junit.jupiter:junit-jupiter-api"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile seleniumDependencies
integrationTestCompile "org.testcontainers:testcontainers"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.junit.jupiter:junit-jupiter-api"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile seleniumDependencies
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -19,32 +19,29 @@ package sample;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;
import org.testcontainers.containers.GenericContainer;
import sample.pages.HomePage;
import sample.pages.LoginPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ellie Bahadori
*/
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class BootTests {
private static final String DOCKER_IMAGE = "hazelcast/hazelcast:latest";
@Autowired
private MockMvc mockMvc;
@@ -60,6 +57,12 @@ class BootTests {
this.driver.quit();
}
@Test // gh-1905
void contextLoads(ApplicationContext context) {
assertThat(context.getBeansOfType(HazelcastIndexedSessionRepository.class)).hasSize(1);
assertThat(context.getBeansOfType(SessionsEndpoint.class)).hasSize(1);
}
@Test
void home() {
LoginPage login = HomePage.go(this.driver);
@@ -83,16 +86,4 @@ class BootTests {
login.assertAt();
}
@TestConfiguration
static class Config {
@Bean
GenericContainer hazelcastContainer() {
GenericContainer hazelcastContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(5701);
hazelcastContainer.start();
return hazelcastContainer;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -34,7 +34,7 @@ import org.springframework.session.hazelcast.PrincipalNameExtractor;
public class SessionConfig {
@Bean
public Config clientConfig() {
public Config hazelcastConfig() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);

View File

@@ -1 +1,2 @@
management.endpoints.web.exposure.include=sessions
spring.security.user.password=password

View File

@@ -1,20 +1,20 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-session-hazelcast')
compile project(':hazelcast4')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast:4.2.2"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator-core"
compile project(':spring-session-hazelcast')
compile project(':hazelcast4')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast:4.2.2"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator-core"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.junit.jupiter:junit-jupiter-api"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile seleniumDependencies
integrationTestCompile "org.testcontainers:testcontainers"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.junit.jupiter:junit-jupiter-api"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile seleniumDependencies
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -19,29 +19,26 @@ package sample;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;
import org.testcontainers.containers.GenericContainer;
import sample.pages.HomePage;
import sample.pages.LoginPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
@ExtendWith(SpringExtension.class)
import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class BootTests {
private static final String DOCKER_IMAGE = "hazelcast/hazelcast:latest";
@Autowired
private MockMvc mockMvc;
@@ -57,6 +54,12 @@ class BootTests {
this.driver.quit();
}
@Test // gh-1905
void contextLoads(ApplicationContext context) {
assertThat(context.getBeansOfType(Hazelcast4IndexedSessionRepository.class)).hasSize(1);
assertThat(context.getBeansOfType(SessionsEndpoint.class)).hasSize(1);
}
@Test
void home() {
LoginPage login = HomePage.go(this.driver);
@@ -80,16 +83,4 @@ class BootTests {
login.assertAt();
}
@TestConfiguration
static class Config {
@Bean
GenericContainer hazelcastContainer() {
GenericContainer hazelcastContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(5701);
hazelcastContainer.start();
return hazelcastContainer;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* Copyright 2014-2021 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.
@@ -35,7 +35,7 @@ import org.springframework.session.hazelcast.HazelcastSessionSerializer;
public class SessionConfig {
@Bean
public Config clientConfig() {
public Config hazelcastConfig() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);

View File

@@ -1 +1,2 @@
management.endpoints.web.exposure.include=sessions
spring.security.user.password=password