Compare commits

..

41 Commits
2.4.4 ... 2.3.x

Author SHA1 Message Date
Eleftheria Stein
9f8403659e Add End-of-Life Notice 2021-12-06 16:25:48 +01:00
Eleftheria Stein
8cc3fd14e1 Remove from CI 2021-12-06 16:24:49 +01:00
Eleftheria Stein
6b29fef71a Next development version 2021-04-13 15:19:43 +02:00
Eleftheria Stein
c51aa38087 Release 2.3.3.RELEASE 2021-04-13 14:39:26 +02:00
Eleftheria Stein
ff02a3b7be Upgrade test dependencies 2021-04-13 14:37:39 +02:00
Eleftheria Stein
d02f0279a3 Upgrade Spring Data to Neumann-SR8
Closes gh-1831
2021-04-13 14:37:14 +02:00
Eleftheria Stein
3bb26735b7 Upgrade Spring Security to 5.3.9.RELEASE
Closes gh-1832
2021-04-13 14:36:43 +02:00
Eleftheria Stein
2fa4aecd13 Upgrade Spring Framework to 5.2.14.RELEASE
Closes gh-1834
2021-04-13 14:36:22 +02:00
Eleftheria Stein
8645271d0b Upgrade Hazelcast to 3.12.12
Closes gh-1833
2021-04-13 14:35:09 +02:00
Eleftheria Stein
00ac080069 Upgrade Reactor to Dysprosium-SR19
Closes gh-1830
2021-04-13 14:34:40 +02:00
Eleftheria Stein
e0309c0519 Upgrade samples to Spring Boot 2.2.13.RELEASE
Closes gh-1829
2021-04-13 14:34:03 +02:00
Eleftheria Stein
5f27de9d28 Throw exception if session created after response
Closes gh-1798
2021-03-25 12:25:58 +02:00
Eleftheria Stein
9de05feb9f Checkout 2.3.x branch in CI workflow 2021-02-22 10:59:51 +01:00
Ellie Bahadori
fe32f7a66b Create GitHub Actions CI pipeline 2021-02-19 17:07:06 +01:00
Ellie Bahadori
1b63952e58 Set up Github Actions pipeline for PRs 2021-02-19 17:06:53 +01:00
Eleftheria Stein
94009301d5 Use spring-build-conventions:0.0.37 2021-01-27 12:44:49 +01:00
Eleftheria Stein
ada7a35458 Next development version 2021-01-19 14:49:08 +01:00
Eleftheria Stein
19d4d1d521 Release 2.3.2.RELEASE 2021-01-19 14:27:36 +01:00
Eleftheria Stein
025b6e99dd Upgrade test dependencies 2021-01-19 14:00:33 +01:00
Eleftheria Stein
dd6f5ac7a8 Jenkinsfile JDK 12 performs git clean 2021-01-19 09:20:04 +01:00
Eleftheria Stein
69c1e3f6b2 Revert "Upgrade test dependencies"
This reverts commit 9c77691232.
2021-01-18 17:12:45 +01:00
Eleftheria Stein
9c77691232 Upgrade test dependencies 2021-01-18 14:43:56 +01:00
Eleftheria Stein
8a1f5253ce Upgrade Spring Data to Neumann-SR6
Closes gh-1771
2021-01-18 14:42:17 +01:00
Eleftheria Stein
c44954ea5c Upgrade Spring Security to 5.3.6
Closes gh-1770
2021-01-18 14:40:48 +01:00
Eleftheria Stein
441fc363a1 Upgrade Spring Framework to 5.2.12
Closes gh-1769
2021-01-18 14:39:06 +01:00
Eleftheria Stein
fcf3f4a542 Upgrade Reactor to Dysprosium-SR16
Closes gh-1768
2021-01-18 14:36:51 +01:00
Eleftheria Stein
78684c7ef8 Upgrade Hazelcast to 3.12.11
Closes gh-1767
2021-01-18 14:34:36 +01:00
Eleftheria Stein
6d2ffb173b Update testcontainers to fix Docker connectivity
Closes gh-1751
2021-01-07 11:46:23 +01:00
Eleftheria Stein
35acf0b537 Add artifactory credentials to build 2020-11-18 14:18:56 +01:00
Eleftheria Stein
d6482f36de Next development version 2020-09-16 18:03:13 +02:00
Eleftheria Stein
9992d32dc1 Disable parallel deployment build in Jenkins
Closes gh-1703
2020-09-16 17:29:55 +02:00
Eleftheria Stein
3c912ec87b Release 2.3.1.RELEASE 2020-09-16 16:58:28 +02:00
Eleftheria Stein
a8e8bc5db1 Upgrade Spring Data to Neumann-SR4
Closes gh-1702
2020-09-16 16:50:30 +02:00
Eleftheria Stein
d22983bef8 Upgrade Spring Framework to 5.2.9.RELEASE
Closes gh-1689
2020-09-15 11:34:37 +02:00
Eleftheria Stein
1746ad630b Upgrade test dependencies 2020-09-15 11:19:09 +02:00
Eleftheria Stein
953db5c611 Upgrade Spring Security to 5.3.4.RELEASE
Closes gh-1688
2020-09-15 11:18:40 +02:00
Eleftheria Stein
fb0b2691d2 Upgrade Reactor to Dysprosium-SR12
Closes gh-1687
2020-09-15 11:17:37 +02:00
Eleftheria Stein
2bdc416621 Upgrade samples to Spring Boot 2.2.9.RELEASE
Closes gh-1686
2020-09-15 11:16:15 +02:00
Eleftheria Stein
bbec4bd8e6 Upgrade documentation styling
Resolves gh-1640
2020-08-04 11:07:21 +02:00
Eleftheria Stein
dba5ad7412 Remove JDK 9 and 10 from Jenkins build
Closes gh-1659
2020-07-16 15:23:24 +02:00
Eleftheria Stein
f4038dc5c7 Next development build 2020-05-12 16:47:59 -04:00
87 changed files with 130 additions and 3716 deletions

View File

@@ -1,96 +0,0 @@
name: 2.4.x CI
on:
push:
branches:
- 2.4.x
schedule:
- cron: '4 10 * * *' # Once per day at 10:04am UTC
workflow_dispatch: # Manual trigger
env:
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2
with:
ref: '2.4.x'
- name: Set up JDK ${{ matrix.jdk }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.jdk }}
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew clean build -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --no-daemon --stacktrace
artifacts:
name: Deploy Artifacts
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: '2.4.x'
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- name: Deploy artifacts
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
export VERSION_HEADER=$'Version: GnuPG v2\n\n'
export ORG_GRADLE_PROJECT_signingKey=${GPG_PRIVATE_KEY#"$VERSION_HEADER"}
export ORG_GRADLE_PROJECT_signingPassword="$GPG_PASSPHRASE"
./gradlew deployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace --no-parallel
./gradlew finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace --no-parallel
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_TOKEN_USERNAME }}
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_TOKEN_PASSWORD }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
docs:
name: Deploy Docs
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: '2.4.x'
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- name: Deploy Docs
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
./gradlew deployDocs --no-daemon -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
env:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}

View File

@@ -1,6 +1,9 @@
name: PR Build
name: 2.3.x PR Build
on: pull_request
on:
pull_request:
branches:
- 2.3.x
jobs:
build:

View File

@@ -1,3 +1,9 @@
[NOTE]
======
This branch of Spring Session has reached its https://github.com/spring-projects/spring-boot/wiki/Supported-Versions[End of Life], meaning that there are no further maintenance releases or security patches planned.
Please migrate to a supported branch as soon as possible.
======
= Spring Session
image:https://badges.gitter.im/spring-projects/spring-session.svg[link="https://gitter.im/spring-projects/spring-session?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"]

View File

@@ -4,13 +4,12 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.4.7'
springBootVersion = '2.2.13.RELEASE'
}
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release/' }
maven { url 'https://repo.spring.io/plugins-snapshot' }
}
dependencies {

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.4.4
version=2.3.4.BUILD-SNAPSHOT

View File

@@ -1,11 +1,11 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:2020.0.7'
mavenBom 'org.junit:junit-bom:5.7.1'
mavenBom 'org.springframework:spring-framework-bom:5.3.8'
mavenBom 'org.springframework.data:spring-data-bom:2020.0.9'
mavenBom 'org.springframework.security:spring-security-bom:5.4.6'
mavenBom 'org.testcontainers:testcontainers-bom:1.15.3'
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-SR19'
mavenBom 'org.junit:junit-bom:5.6.3'
mavenBom 'org.springframework:spring-framework-bom:5.2.14.RELEASE'
mavenBom 'org.springframework.data:spring-data-releasetrain:Neumann-SR8'
mavenBom 'org.springframework.security:spring-security-bom:5.3.9.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.15.2'
}
dependencies {
@@ -14,23 +14,22 @@ dependencyManagement {
entry 'hazelcast-client'
}
dependency 'org.aspectj:aspectjweaver:1.9.6'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.0.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.4.1.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:19.8.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'com.oracle.ojdbc:ojdbc8:19.3.0.0'
dependency 'com.zaxxer:HikariCP:3.4.1'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:6.0.5.RELEASE'
dependency 'io.lettuce:lettuce-core:5.2.2.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.23'
dependency 'junit:junit:4.13'
dependency 'mysql:mysql-connector-java:8.0.21'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.18.0'
dependency 'org.assertj:assertj-core:3.15.0'
dependency 'org.hsqldb:hsqldb:2.5.1'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.3'
dependency 'org.mockito:mockito-core:3.5.15'
dependency 'org.postgresql:postgresql:42.2.18'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.4'
dependency 'org.mockito:mockito-core:3.3.3'
dependency 'org.postgresql:postgresql:42.2.16'
}
}

View File

@@ -5,8 +5,6 @@ include 'spring-session-data-redis'
include 'spring-session-docs'
include 'spring-session-hazelcast'
include 'spring-session-jdbc'
include 'hazelcast4'
project(':hazelcast4').projectDir = file('spring-session-hazelcast/hazelcast4')
file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
include dir.name

View File

@@ -25,6 +25,5 @@ dependencies {
testCompile "org.springframework.security:spring-security-core"
testCompile "org.junit.jupiter:junit-jupiter-api"
testCompile "org.junit.jupiter:junit-jupiter-params"
testCompile "org.aspectj:aspectjweaver"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
}

View File

@@ -64,7 +64,7 @@ abstract class OncePerRequestFilter implements Filter {
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName;
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2014-2020 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.web.http;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.OncePerRequestFilterAopTests.Config;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThatCode;
@SpringJUnitConfig(classes = Config.class)
class OncePerRequestFilterAopTests {
@Test
void doFilterOnce(@Autowired final OncePerRequestFilter filter) {
assertThatCode(() -> filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockFilterChain())).as("`doFilter` does not throw NPE with the bean is being proxied by Spring AOP")
.doesNotThrowAnyException();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Aspect
public static class Config {
@Bean
public SessionRepository sessionRepository() {
return Mockito.mock(SessionRepository.class);
}
@Bean
public SessionRepositoryFilter filter() {
return new SessionRepositoryFilter(sessionRepository());
}
@AfterReturning("execution(* SessionRepositoryFilter.doFilterInternal(..))")
public void doInternalFilterPointcut() {
// no op
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* 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.

View File

@@ -28,7 +28,7 @@ Consider the following scenario:
* User goes to library and authenticates to the application.
* User goes home and realizes they forgot to log out.
* User can log in and end the session from the library using clues like the location, created time, last accessed time, and so on.
* User can log in and terminate the session from the library using clues like the location, created time, last accessed time, and so on.
Would it not be nice if we could let the user invalidate the session at the library from any device with which they authenticate?
This sample demonstrates how this is possible.
@@ -145,5 +145,5 @@ You can emulate the flow we discussed in the <<About the Sample>> section by doi
* Enter the following to log in:
** *Username* _user_
** *Password* _password_
* End your original session.
* Terminate your original session.
* Refresh the original window and see that you are logged out.

View File

@@ -59,14 +59,14 @@ What does `AbstractSessionWebSocketMessageBrokerConfigurer` do behind the scenes
* `WebSocketConnectHandlerDecoratorFactory` is added as a `WebSocketHandlerDecoratorFactory` to `WebSocketTransportRegistration`.
This ensures a custom `SessionConnectEvent` is fired that contains the `WebSocketSession`.
The `WebSocketSession` is necessary to end any WebSocket connections that are still open when a Spring Session is ended.
The `WebSocketSession` is necessary to terminate any WebSocket connections that are still open when a Spring Session is terminated.
* `SessionRepositoryMessageInterceptor` is added as a `HandshakeInterceptor` to every `StompWebSocketEndpointRegistration`.
This ensures that the `Session` is added to the WebSocket properties to enable updating the last accessed time.
* `SessionRepositoryMessageInterceptor` is added as a `ChannelInterceptor` to our inbound `ChannelRegistration`.
This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.
* `WebSocketRegistryListener` is created as a Spring bean.
This ensures that we have a mapping of all of the `Session` IDs to the corresponding WebSocket connections.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is ended.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is terminated.
// end::config[]
@@ -131,7 +131,7 @@ You can see that the message is no longer sent.
====
Spring Session expires in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds.
To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions.
This means you need to wait at most two minutes before the WebSocket connection is closed.
This means you need to wait at most two minutes before the WebSocket connection is terminated.
====
You can now try accessing http://localhost:8080/

View File

@@ -97,18 +97,11 @@ The filter is in charge of replacing the `HttpSession` implementation to be back
In this instance, Spring Session is backed by Hazelcast.
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
Spring Session provides `PrincipalNameExtractor` for this purpose.
<3> In order to serialize `MapSession` objects efficiently, `HazelcastSessionSerializer` needs to be registered. If this
is not set, Hazelcast will serialize sessions using native Java serialization.
<4> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
<3> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
By default, the application starts and connects to an embedded instance of Hazelcast.
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
====
NOTE: If `HazelcastSessionSerializer` is preferred, it needs to be configured for all Hazelcast cluster members before they start.
In a Hazelcast cluster, all members should use the same serialization method for sessions. Also, if Hazelcast Client/Server topology
is used, then both members and clients must use the same serialization method. The serializer can be registered via `ClientConfig`
with the same `SerializerConfiguration` of members.
== Servlet Container Initialization
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.

View File

@@ -128,36 +128,6 @@ Doing so ensures that the Spring bean named `springSessionRepositoryFilter` is r
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to ensure Spring loads our `Config`.
====
== Multiple DataSources
Spring Session provides the `@SpringSessionDataSource` qualifier, allowing you to explicitly declare which `DataSource` bean should be injected in `JdbcIndexedSessionRepository`.
This is particularly useful in scenarios with multiple `DataSource` beans present in the application context.
The following example shows how to do so:
====
.Config.java
[source,java]
----
@EnableJdbcHttpSession
public class Config {
@Bean
@SpringSessionDataSource // <1>
public EmbeddedDatabase firstDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2).addScript("org/springframework/session/jdbc/schema-h2.sql").build();
}
@Bean
public HikariDataSource secondDataSource() {
// ...
}
}
----
<1> This qualifier declares that firstDataSource is to be used by Spring Session.
====
// end::config[]
[[httpsession-jdbc-sample]]

View File

@@ -58,10 +58,6 @@ To get started with Spring Session, the best place to start is our Sample Applic
| 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]
@@ -1181,8 +1177,8 @@ include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schem
==== Transaction Management
All JDBC operations in `JdbcIndexedSessionRepository` are performed in a transactional manner.
Transactions are performed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
All JDBC operations in `JdbcIndexedSessionRepository` are executed in a transactional manner.
Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
[[api-hazelcastindexedsessionrepository]]
=== Using `HazelcastIndexedSessionRepository`

View File

@@ -19,15 +19,12 @@ 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;
@@ -45,10 +42,7 @@ public class HazelcastHttpSessionConfig {
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>
return Hazelcast.newHazelcastInstance(config); // <3>
}
}

View File

@@ -1,48 +0,0 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE'
}
}
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.dependency-set'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
dependencies {
compile project(':spring-session-core')
optional "com.hazelcast:hazelcast:4.0.3"
compile "org.springframework:spring-context"
compile "javax.annotation:javax.annotation-api"
testCompile "javax.servlet:javax.servlet-api"
testCompile "org.springframework:spring-web"
testCompile "org.junit.jupiter:junit-jupiter-api"
testCompile "org.springframework.security:spring-security-core"
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile "org.testcontainers:testcontainers"
integrationTestCompile "com.hazelcast:hazelcast:4.0.3"
integrationTestCompile project(":spring-session-hazelcast")
}

View File

@@ -1,201 +0,0 @@
/*
* 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 com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.MapSession;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for {@link Hazelcast4IndexedSessionRepository} integration tests.
*
* @author Eleftheria Stein
*/
abstract class AbstractHazelcast4IndexedSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Autowired
private HazelcastInstance hazelcastInstance;
@Autowired
private Hazelcast4IndexedSessionRepository repository;
@Test
void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.deleteById(sessionId);
assertThat(hazelcastMap.get(sessionId)).isNull();
}
@Test
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenChangeTwice() {
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
String originalId = toSave.getId();
String changeId1 = toSave.changeSessionId();
String changeId2 = toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(originalId)).isNull();
assertThat(this.repository.findById(changeId1)).isNull();
assertThat(this.repository.findById(changeId2)).isNotNull();
this.repository.deleteById(changeId2);
}
@Test
void changeSessionIdWhenSetAttributeOnChangedSession() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
findById.setAttribute(attrName, attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenHasNotSaved() {
HazelcastSession toSave = this.repository.createSession();
String originalId = toSave.getId();
toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(toSave.getId())).isNotNull();
assertThat(this.repository.findById(originalId)).isNull();
this.repository.deleteById(toSave.getId());
}
@Test // gh-1076
void attemptToUpdateSessionAfterDelete() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.deleteById(sessionId);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNull();
}
@Test
void createAndUpdateSession() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
void createSessionWithSecurityContextAndFindById() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken("saves-" + System.currentTimeMillis(),
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2014-2020 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 com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
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;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using client-server
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class ClientServerHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:4.0.2")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml");
@BeforeAll
static void setUpClass() {
container.start();
}
@AfterAll
static void tearDownClass() {
container.stop();
}
@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig()
.addAddress(container.getContainerIpAddress() + ":" + container.getFirstMappedPort());
clientConfig.getUserCodeDeploymentConfig().setEnabled(true).addClass(Session.class)
.addClass(MapSession.class).addClass(Hazelcast4SessionUpdateEntryProcessor.class);
return HazelcastClient.newHazelcastClient(clientConfig);
}
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2014-2020 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 com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using embedded
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class EmbeddedHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2014-2020 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 com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.IndexConfig;
import com.hazelcast.config.IndexType;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.MapSession;
/**
* Utility class for Hazelcast integration tests.
*
* @author Eleftheria Stein
*/
final class Hazelcast4ITestUtils {
private Hazelcast4ITestUtils() {
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @return the Hazelcast instance
*/
static HazelcastInstance embeddedHazelcastServer() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
AttributeConfig attributeConfig = new AttributeConfig()
.setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);
return Hazelcast.newHazelcastInstance(config);
}
}

View File

@@ -1,218 +0,0 @@
/*
* Copyright 2014-2020 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 com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.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.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
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;
/**
* Ensure that the appropriate SessionEvents are fired at the expected times. Additionally
* ensure that the interactions with the {@link SessionRepository} abstraction behave as
* expected after each SessionEvent.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SessionEventHazelcast4IndexedSessionRepositoryTests<S extends Session> {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
@Autowired
private SessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@BeforeEach
void setup() {
this.registry.clear();
}
@Test
void saveSessionTest() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
S sessionToSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
Session session = this.repository.findById(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(sessionToSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(sessionToSave.getAttribute(expectedAttributeName));
}
@Test
void expiredSessionTest() 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();
assertThat(sessionToSave.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void deletedSessionTest() 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();
this.repository.deleteById(sessionToSave.getId());
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionDeletedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionDeletedEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void saveUpdatesTimeToLiveTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofSeconds(3));
this.repository.save(sessionToSave);
Thread.sleep(2000);
// Get and save the session like SessionRepositoryFilter would.
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
Thread.sleep(2000);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNotNull();
}
@Test // gh-1077
void changeSessionIdNoEventTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
sessionToSave.changeSessionId();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isFalse();
}
@Test // gh-1300
void updateMaxInactiveIntervalTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
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());
sessionToUpdate.setMaxInactiveInterval(Duration.ofSeconds(1));
this.repository.save(sessionToUpdate);
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {
@Bean
HazelcastInstance embeddedHazelcast() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
@Bean
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2014-2020 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.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.context.ApplicationListener;
import org.springframework.session.events.AbstractSessionEvent;
class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
private Map<String, AbstractSessionEvent> events = new HashMap<>();
private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(AbstractSessionEvent event) {
String sessionId = event.getSessionId();
this.events.put(sessionId, event);
Object lock = getLock(sessionId);
synchronized (lock) {
lock.notifyAll();
}
}
void clear() {
this.events.clear();
this.locks.clear();
}
boolean receivedEvent(String sessionId) throws InterruptedException {
return waitForEvent(sessionId) != null;
}
@SuppressWarnings("unchecked")
<E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
return (E) waitForEvent(sessionId);
}
@SuppressWarnings("unchecked")
private <E extends AbstractSessionEvent> E waitForEvent(String sessionId) throws InterruptedException {
Object lock = getLock(sessionId);
synchronized (lock) {
if (!this.events.containsKey(sessionId)) {
lock.wait(10000);
}
}
return (E) this.events.get(sessionId);
}
private Object getLock(String sessionId) {
return this.locks.computeIfAbsent(sessionId, (k) -> new Object());
}
}

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-4.0.xsd">
<network>
<join>
<multicast enabled="false"/>
</join>
</network>
<user-code-deployment enabled="true">
<class-cache-mode>ETERNAL</class-cache-mode>
<provider-mode>LOCAL_AND_CACHED_CLASSES</provider-mode>
</user-code-deployment>
</hazelcast>

View File

@@ -1,486 +0,0 @@
/*
* Copyright 2014-2020 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryExpiredListener;
import com.hazelcast.map.listener.EntryRemovedListener;
import com.hazelcast.query.Predicates;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent;
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
* 4 that stores sessions in Hazelcast's distributed {@link IMap}.
*
* <p>
* An example of how to create a new instance can be seen below:
*
* <pre class="code">
* Config config = new Config();
*
* // ... configure Hazelcast ...
*
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
*
* Hazelcast4IndexedSessionRepository sessionRepository =
* new Hazelcast4IndexedSessionRepository(hazelcastInstance);
* </pre>
*
* In order to support finding sessions by principal name using
* {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of
* {@code IMap} supplied to this implementation is required.
*
* The following snippet demonstrates how to define required configuration using
* programmatic Hazelcast Configuration:
*
* <pre class="code">
* AttributeConfig attributeConfig = new AttributeConfig()
* .setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .addAttributeConfig(attributeConfig)
* .addIndexConfig(new IndexConfig(
* IndexType.HASH,
* Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
*
* Hazelcast.newHazelcastInstance(config);
* </pre>
*
* This implementation listens for events on the Hazelcast-backed SessionRepository and
* translates those events into the corresponding Spring Session events. Publish the
* Spring Session events with the given {@link ApplicationEventPublisher}.
*
* <ul>
* <li>entryAdded - {@link SessionCreatedEvent}</li>
* <li>entryEvicted - {@link SessionExpiredEvent}</li>
* <li>entryExpired - {@link SessionExpiredEvent}</li>
* <li>entryRemoved - {@link SessionDeletedEvent}</li>
* </ul>
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4IndexedSessionRepository
implements FindByIndexNameSessionRepository<Hazelcast4IndexedSessionRepository.HazelcastSession>,
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession>, EntryExpiredListener<String, MapSession> {
/**
* The default name of map used by Spring Session to store sessions.
*/
public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
/**
* The principal name custom attribute name.
*/
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
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;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override
* {@link MapSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private IMap<String, MapSession> sessions;
private UUID sessionListenerId;
/**
* Create a new {@link Hazelcast4IndexedSessionRepository} instance.
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
*/
public Hazelcast4IndexedSessionRepository(HazelcastInstance hazelcastInstance) {
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
this.hazelcastInstance = hazelcastInstance;
}
@PostConstruct
public void init() {
this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
this.sessionListenerId = this.sessions.addEntryListener(this, true);
}
@PreDestroy
public void close() {
this.sessions.removeEntryListener(this.sessionListenerId);
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link AbstractSessionEvent session events}. The default is to not publish session
* events.
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish session events. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Set the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
*/
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Set the {@link IndexResolver} to use.
* @param indexResolver the index resolver
*/
public void setIndexResolver(IndexResolver<Session> indexResolver) {
Assert.notNull(indexResolver, "indexResolver cannot be null");
this.indexResolver = indexResolver;
}
/**
* Set the name of map used to store sessions.
* @param sessionMapName the session map name
*/
public void setSessionMapName(String sessionMapName) {
Assert.hasText(sessionMapName, "Map name must not be empty");
this.sessionMapName = sessionMapName;
}
/**
* Sets the Hazelcast flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the new Hazelcast flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public HazelcastSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
HazelcastSession session = new HazelcastSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
public void save(HazelcastSession session) {
if (session.isNew) {
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.sessionIdChanged) {
this.sessions.delete(session.originalId);
session.originalId = session.getId();
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.hasChanges()) {
Hazelcast4SessionUpdateEntryProcessor entryProcessor = new Hazelcast4SessionUpdateEntryProcessor();
if (session.lastAccessedTimeChanged) {
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
entryProcessor.setDelta(new HashMap<>(session.delta));
}
this.sessions.executeOnKey(session.getId(), entryProcessor);
}
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);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
deleteById(saved.getId());
return null;
}
return new HazelcastSession(saved, false);
}
@Override
public void deleteById(String id) {
this.sessions.remove(id);
}
@Override
public Map<String, HazelcastSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size());
for (MapSession session : sessions) {
sessionMap.put(session.getId(), new HazelcastSession(session, false));
}
return sessionMap;
}
@Override
public void entryAdded(EntryEvent<String, MapSession> event) {
MapSession session = event.getValue();
if (session.getId().equals(session.getOriginalId())) {
if (logger.isDebugEnabled()) {
logger.debug("Session created with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
}
}
@Override
public void entryEvicted(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
}
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
@Override
public void entryRemoved(EntryEvent<String, MapSession> event) {
MapSession session = event.getOldValue();
if (session != null) {
if (logger.isDebugEnabled()) {
logger.debug("Session deleted with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
}
}
@Override
public void entryExpired(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
}
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track if changes have been made since last save.
*
* @author Aleksandar Stojsavljevic
*/
final class HazelcastSession implements Session {
private final MapSession delegate;
private boolean isNew;
private boolean sessionIdChanged;
private boolean lastAccessedTimeChanged;
private boolean maxInactiveIntervalChanged;
private String originalId;
private Map<String, Object> delta = new HashMap<>();
HazelcastSession(MapSession cached, boolean isNew) {
this.delegate = cached;
this.isNew = isNew;
this.originalId = cached.getId();
if (this.isNew || (Hazelcast4IndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames()
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.lastAccessedTimeChanged = true;
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
@Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public String changeSessionId() {
String newSessionId = this.delegate.changeSessionId();
this.sessionIdChanged = true;
return newSessionId;
}
@Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.maxInactiveIntervalChanged = true;
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.delegate.getAttribute(attributeName);
if (attributeValue != null
&& Hazelcast4IndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.delta.put(attributeName, attributeValue);
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
Map<String, String> indexes = Hazelcast4IndexedSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
}
MapSession getDelegate() {
return this.delegate;
}
boolean hasChanges() {
return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
}
void clearChangeFlags() {
this.isNew = false;
this.lastAccessedTimeChanged = false;
this.sessionIdChanged = false;
this.maxInactiveIntervalChanged = false;
this.delta.clear();
}
private void flushImmediateIfNecessary() {
if (Hazelcast4IndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
Hazelcast4IndexedSessionRepository.this.save(this);
}
}
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2014-2020 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 com.hazelcast.query.extractor.ValueCollector;
import com.hazelcast.query.extractor.ValueExtractor;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
/**
* Hazelcast {@link ValueExtractor} responsible for extracting principal name from the
* {@link MapSession} to be used with Hazelcast 4.
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4PrincipalNameExtractor implements ValueExtractor<MapSession, String> {
@Override
@SuppressWarnings("unchecked")
public void extract(MapSession target, String argument, ValueCollector collector) {
String principalName = target.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
collector.addObject(principalName);
}
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright 2014-2020 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.Map;
import java.util.concurrent.TimeUnit;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.ExtendedMapEntry;
import org.springframework.session.MapSession;
/**
* Hazelcast {@link EntryProcessor} responsible for handling updates to session when using
* Hazelcast 4.
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4SessionUpdateEntryProcessor implements EntryProcessor<String, MapSession, Object> {
private Instant lastAccessedTime;
private Duration maxInactiveInterval;
private Map<String, Object> delta;
@Override
public Object process(Map.Entry<String, MapSession> entry) {
MapSession value = entry.getValue();
if (value == null) {
return Boolean.FALSE;
}
if (this.lastAccessedTime != null) {
value.setLastAccessedTime(this.lastAccessedTime);
}
if (this.maxInactiveInterval != null) {
value.setMaxInactiveInterval(this.maxInactiveInterval);
}
if (this.delta != null) {
for (final Map.Entry<String, Object> attribute : this.delta.entrySet()) {
if (attribute.getValue() != null) {
value.setAttribute(attribute.getKey(), attribute.getValue());
}
else {
value.removeAttribute(attribute.getKey());
}
}
}
if (this.maxInactiveInterval != null) {
((ExtendedMapEntry<String, MapSession>) entry).setValue(value, this.maxInactiveInterval.getSeconds(),
TimeUnit.SECONDS);
}
else {
entry.setValue(value);
}
return Boolean.TRUE;
}
void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}
void setMaxInactiveInterval(Duration maxInactiveInterval) {
this.maxInactiveInterval = maxInactiveInterval;
}
void setDelta(Map<String, Object> delta) {
this.delta = delta;
}
}

View File

@@ -1,472 +0,0 @@
/*
* Copyright 2014-2020 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.IMap;
import com.hazelcast.map.listener.MapListener;
import com.hazelcast.query.impl.predicates.EqualPredicate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository.HazelcastSession;
import org.springframework.test.util.ReflectionTestUtils;
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;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link Hazelcast4IndexedSessionRepository}.
*
* @author Eleftheria Stein
*/
class Hazelcast4IndexedSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
@SuppressWarnings("unchecked")
private IMap<String, MapSession> sessions = mock(IMap.class);
private Hazelcast4IndexedSessionRepository repository;
@BeforeEach
void setUp() {
given(this.hazelcastInstance.<String, MapSession>getMap(anyString())).willReturn(this.sessions);
this.repository = new Hazelcast4IndexedSessionRepository(this.hazelcastInstance);
this.repository.init();
}
@Test
void constructorNullHazelcastInstance() {
assertThatIllegalArgumentException().isThrownBy(() -> new Hazelcast4IndexedSessionRepository(null))
.withMessage("HazelcastInstance must not be null");
}
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test
void createSessionDefaultMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval()).isEqualTo(new MapSession().getMaxInactiveInterval());
verifyZeroInteractions(this.sessions);
}
@Test
void createSessionCustomMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(interval));
verifyZeroInteractions(this.sessions);
}
@Test
void saveNewFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void saveNewFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedAttributeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedAttributeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
void removeAttributeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void removeAttributeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedLastAccessedTimeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedLastAccessedTimeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedMaxInactiveIntervalInSecondsFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyZeroInteractions(this.sessions);
}
@Test
void saveUpdatedMaxInactiveIntervalInSecondsFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
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);
verifyZeroInteractions(this.sessions);
}
@Test
void saveUnchangedFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
void saveUnchangedFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
void getSessionNotFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String sessionId = "testSessionId";
HazelcastSession session = this.repository.findById(sessionId);
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(sessionId));
verifyZeroInteractions(this.sessions);
}
@Test
void getSessionExpired() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
MapSession expired = new MapSession();
expired.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
given(this.sessions.get(eq(expired.getId()))).willReturn(expired);
HazelcastSession session = this.repository.findById(expired.getId());
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(expired.getId()));
verify(this.sessions, times(1)).remove(eq(expired.getId()));
verifyZeroInteractions(this.sessions);
}
@Test
void getSessionFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
given(this.sessions.get(eq(saved.getId()))).willReturn(saved);
HazelcastSession session = this.repository.findById(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.<String>getAttribute("savedName")).isEqualTo("savedValue");
verify(this.sessions, times(1)).get(eq(saved.getId()));
verifyZeroInteractions(this.sessions);
}
@Test
void delete() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String sessionId = "testSessionId";
this.repository.deleteById(sessionId);
verify(this.sessions, times(1)).remove(eq(sessionId));
verifyZeroInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValueUnknownIndexName() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String indexValue = "testIndexValue";
Map<String, HazelcastSession> sessions = this.repository.findByIndexNameAndIndexValue("testIndexName",
indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String principal = "username";
Map<String, HazelcastSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
verifyZeroInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<>(2);
MapSession saved1 = new MapSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
MapSession saved2 = new MapSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
given(this.sessions.values(isA(EqualPredicate.class))).willReturn(saved);
Map<String, HazelcastSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
verifyZeroInteractions(this.sessions);
}
@Test // gh-1120
void getAttributeNamesAndRemove() {
HazelcastSession session = this.repository.createSession();
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
for (String attributeName : session.getAttributeNames()) {
session.removeAttribute(attributeName);
}
assertThat(session.getAttributeNames()).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnSetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(1);
verifyZeroInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnGetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(2);
verifyZeroInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeAlways() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(3);
verifyZeroInteractions(this.sessions);
}
}

View File

@@ -1,29 +1,17 @@
apply plugin: 'io.spring.convention.spring-module'
configurations {
hazelcast4
}
dependencies {
compile project(':spring-session-core')
compile "com.hazelcast:hazelcast"
compile "javax.annotation:javax.annotation-api"
compile "org.springframework:spring-context"
hazelcast4(project(path: ":hazelcast4", configuration: 'classesOnlyElements'))
compileOnly(project(":hazelcast4"))
testCompile "javax.servlet:javax.servlet-api"
testCompile "org.springframework:spring-web"
testCompile "org.springframework.security:spring-security-core"
testCompile "org.junit.jupiter:junit-jupiter-api"
testRuntime project(':hazelcast4')
testRuntime "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile "com.hazelcast:hazelcast-client"
integrationTestCompile "org.testcontainers:testcontainers"
}
jar {
from configurations.hazelcast4
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 the original author or authors.
* 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.
@@ -58,13 +58,16 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
assertThat(hazelcastMap.size()).isEqualTo(0);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.size()).isEqualTo(1);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.deleteById(sessionId);
assertThat(hazelcastMap.get(sessionId)).isNull();
assertThat(hazelcastMap.size()).isEqualTo(0);
}
@Test
@@ -180,8 +183,6 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
@@ -198,8 +199,6 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
@@ -221,8 +220,6 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
assertThat(this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username))
.hasSize(1);
this.repository.deleteById(session.getId());
}
}

View File

@@ -20,12 +20,9 @@ import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.MapSession;
/**
* Utility class for Hazelcast integration tests.
*
@@ -51,9 +48,6 @@ final class HazelcastITestUtils {
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.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);
return Hazelcast.newHazelcastInstance(config);
}

View File

@@ -1,159 +0,0 @@
/*
* Copyright 2014-2020 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.io.EOFException;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.StreamSerializer;
import org.springframework.session.MapSession;
/**
* A {@link com.hazelcast.nio.serialization.Serializer} implementation that handles the
* (de)serialization of {@link MapSession} stored on {@link com.hazelcast.core.IMap}.
*
* <p>
* The use of this serializer is optional and provides faster serialization of sessions.
* If not configured to be used, Hazelcast will serialize sessions via
* {@link java.io.Serializable} by default.
*
* <p>
* If multiple instances of a Spring application is run, then all of them need to use the
* same serialization method. If this serializer is registered on one instance and not
* another one, then it will end up with HazelcastSerializationException. The same applies
* when clients are configured to use this serializer but not the members, and vice versa.
* Also note that, if a new instance is created with this serialization but the existing
* Hazelcast cluster contains the values not serialized by this but instead the default
* one, this will result in incompatibility again.
*
* <p>
* An example of how to register the serializer on embedded instance can be seen below:
*
* <pre class="code">
* Config config = new Config();
*
* // ... other configurations for Hazelcast ...
*
* SerializerConfig serializerConfig = new SerializerConfig();
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
* config.getSerializationConfig().addSerializerConfig(serializerConfig);
*
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
* </pre>
*
* Below is the example of how to register the serializer on client instance. Note that,
* to use the serializer in client/server mode, the serializer - and hence
* {@link MapSession}, must exist on the server's classpath and must be registered via
* {@link com.hazelcast.config.SerializerConfig} with the configuration above for each
* server.
*
* <pre class="code">
* ClientConfig clientConfig = new ClientConfig();
*
* // ... other configurations for Hazelcast Client ...
*
* SerializerConfig serializerConfig = new SerializerConfig();
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
* clientConfig.getSerializationConfig().addSerializerConfig(serializerConfig);
*
* HazelcastInstance hazelcastClient = HazelcastClient.newHazelcastClient(clientConfig);
* </pre>
*
* @author Enes Ozcan
* @since 2.4.0
*/
public class HazelcastSessionSerializer implements StreamSerializer<MapSession> {
private static final int SERIALIZER_TYPE_ID = 1453;
@Override
public void write(ObjectDataOutput out, MapSession session) throws IOException {
out.writeUTF(session.getOriginalId());
out.writeUTF(session.getId());
writeInstant(out, session.getCreationTime());
writeInstant(out, session.getLastAccessedTime());
writeDuration(out, session.getMaxInactiveInterval());
for (String attrName : session.getAttributeNames()) {
Object attrValue = session.getAttribute(attrName);
if (attrValue != null) {
out.writeUTF(attrName);
out.writeObject(attrValue);
}
}
}
private void writeInstant(ObjectDataOutput out, Instant instant) throws IOException {
out.writeLong(instant.getEpochSecond());
out.writeInt(instant.getNano());
}
private void writeDuration(ObjectDataOutput out, Duration duration) throws IOException {
out.writeLong(duration.getSeconds());
out.writeInt(duration.getNano());
}
@Override
public MapSession read(ObjectDataInput in) throws IOException {
String originalId = in.readUTF();
MapSession cached = new MapSession(originalId);
cached.setId(in.readUTF());
cached.setCreationTime(readInstant(in));
cached.setLastAccessedTime(readInstant(in));
cached.setMaxInactiveInterval(readDuration(in));
try {
while (true) {
// During write, it's not possible to write
// number of non-null attributes without an extra
// iteration. Hence the attributes are read until
// EOF here.
String attrName = in.readUTF();
Object attrValue = in.readObject();
cached.setAttribute(attrName, attrValue);
}
}
catch (EOFException ignored) {
}
return cached;
}
private Instant readInstant(ObjectDataInput in) throws IOException {
long seconds = in.readLong();
int nanos = in.readInt();
return Instant.ofEpochSecond(seconds, nanos);
}
private Duration readDuration(ObjectDataInput in) throws IOException {
long seconds = in.readLong();
int nanos = in.readInt();
return Duration.ofSeconds(seconds, nanos);
}
@Override
public int getTypeId() {
return SERIALIZER_TYPE_ID;
}
@Override
public void destroy() {
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -80,10 +81,11 @@ public @interface EnableHazelcastHttpSession {
/**
* This is the name of the Map that will be used in Hazelcast to store the session
* data. Default is "spring:session:sessions".
* data. Default is
* {@link HazelcastIndexedSessionRepository#DEFAULT_SESSION_MAP_NAME}.
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default "spring:session:sessions";
String sessionMapName() default HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
/**
* Flush mode for the Hazelcast sessions. The default is {@code ON_SAVE} which only

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* 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.
@@ -35,15 +35,12 @@ 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;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -75,23 +72,23 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
private List<SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>> sessionRepositoryCustomizers;
private List<SessionRepositoryCustomizer<Hazelcast4IndexedSessionRepository>> hazelcast4SessionRepositoryCustomizers;
private static final boolean hazelcast4;
static {
ClassLoader classLoader = HazelcastHttpSessionConfiguration.class.getClassLoader();
hazelcast4 = ClassUtils.isPresent("com.hazelcast.map.IMap", classLoader);
}
@Bean
public SessionRepository<?> sessionRepository() {
if (hazelcast4) {
return createHazelcast4IndexedSessionRepository();
public HazelcastIndexedSessionRepository sessionRepository() {
HazelcastIndexedSessionRepository sessionRepository = new HazelcastIndexedSessionRepository(
this.hazelcastInstance);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
else {
return createHazelcastIndexedSessionRepository();
if (StringUtils.hasText(this.sessionMapName)) {
sessionRepository.setSessionMapName(this.sessionMapName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
@@ -142,13 +139,6 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Autowired(required = false)
public void setHazelcast4SessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<Hazelcast4IndexedSessionRepository>> sessionRepositoryCustomizers) {
this.hazelcast4SessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream()
.collect(Collectors.toList());
}
@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {
@@ -169,40 +159,4 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.saveMode = attributes.getEnum("saveMode");
}
private HazelcastIndexedSessionRepository createHazelcastIndexedSessionRepository() {
HazelcastIndexedSessionRepository sessionRepository = new HazelcastIndexedSessionRepository(
this.hazelcastInstance);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (StringUtils.hasText(this.sessionMapName)) {
sessionRepository.setSessionMapName(this.sessionMapName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
private Hazelcast4IndexedSessionRepository createHazelcast4IndexedSessionRepository() {
Hazelcast4IndexedSessionRepository sessionRepository = new Hazelcast4IndexedSessionRepository(
this.hazelcastInstance);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (StringUtils.hasText(this.sessionMapName)) {
sessionRepository.setSessionMapName(this.sessionMapName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
this.hazelcast4SessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2014-2020 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.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.config.SerializationConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.internal.serialization.impl.DefaultSerializationServiceBuilder;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.spi.serialization.SerializationService;
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;
class HazelcastSessionSerializerTests {
private SerializationService serializationService;
@BeforeEach
void setUp() {
SerializationConfig serializationConfig = new SerializationConfig();
SerializerConfig serializerConfig = new SerializerConfig().setImplementation(new HazelcastSessionSerializer())
.setTypeClass(MapSession.class);
serializationConfig.addSerializerConfig(serializerConfig);
this.serializationService = new DefaultSerializationServiceBuilder().setConfig(serializationConfig).build();
}
@Test
void serializeSessionWithStreamSerializer() {
MapSession originalSession = new MapSession();
originalSession.setAttribute("attr1", "value1");
originalSession.setAttribute("attr2", "value2");
originalSession.setAttribute("attr3", new SerializableTestAttribute(3));
originalSession.setMaxInactiveInterval(Duration.ofDays(5));
originalSession.setLastAccessedTime(Instant.now());
originalSession.setId("custom-id");
Data serialized = this.serializationService.toData(originalSession);
MapSession cached = this.serializationService.toObject(serialized);
assertThat(originalSession.getCreationTime()).isEqualTo(cached.getCreationTime());
assertThat(originalSession.getMaxInactiveInterval()).isEqualTo(cached.getMaxInactiveInterval());
assertThat(originalSession.getId()).isEqualTo(cached.getId());
assertThat(originalSession.getOriginalId()).isEqualTo(cached.getOriginalId());
assertThat(originalSession.getAttributeNames().size()).isEqualTo(cached.getAttributeNames().size());
assertThat(originalSession.<String>getAttribute("attr1")).isEqualTo(cached.getAttribute("attr1"));
assertThat(originalSession.<String>getAttribute("attr2")).isEqualTo(cached.getAttribute("attr2"));
assertThat(originalSession.<SerializableTestAttribute>getAttribute("attr3"))
.isEqualTo(cached.getAttribute("attr3"));
}
static class SerializableTestAttribute implements Serializable {
private int id;
SerializableTestAttribute(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SerializableTestAttribute)) {
return false;
}
SerializableTestAttribute that = (SerializableTestAttribute) o;
return this.id == that.id;
}
@Override
public int hashCode() {
return this.id;
}
}
}

View File

@@ -16,7 +16,7 @@ dependencies {
integrationTestCompile "com.h2database:h2"
integrationTestCompile "com.ibm.db2:jcc"
integrationTestCompile "com.microsoft.sqlserver:mssql-jdbc"
integrationTestCompile "com.oracle.database.jdbc:ojdbc8"
integrationTestCompile "com.oracle.ojdbc:ojdbc8"
integrationTestCompile "com.zaxxer:HikariCP"
integrationTestCompile "mysql:mysql-connector-java"
integrationTestCompile "org.apache.derby:derby"

View File

@@ -118,7 +118,7 @@ final class DatabaseContainers {
private static class MySql5Container extends MySQLContainer<MySql5Container> {
MySql5Container() {
super("mysql:5.7.34");
super("mysql:5.7.27");
}
@Override
@@ -137,7 +137,7 @@ final class DatabaseContainers {
private static class MySql8Container extends MySQLContainer<MySql8Container> {
MySql8Container() {
super("mysql:8.0.25");
super("mysql:8.0.17");
}
@Override
@@ -174,7 +174,7 @@ final class DatabaseContainers {
private static class PostgreSql9Container extends PostgreSQLContainer<PostgreSql9Container> {
PostgreSql9Container() {
super("postgres:9.6.22");
super("postgres:9.6.15");
}
}

View File

@@ -44,7 +44,6 @@ import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobCreator;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
@@ -461,65 +460,63 @@ public class JdbcIndexedSessionRepository
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
lobCreator.setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
lobCreator.setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
lobCreator.setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
lobCreator.setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
});
}
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
});
}
}

View File

@@ -35,8 +35,6 @@ import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.TemporaryLobCreator;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -55,8 +53,6 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -714,19 +710,4 @@ class JdbcIndexedSessionRepositoryTests {
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveAndFreeTemporaryLob() {
DefaultLobHandler lobHandler = mock(DefaultLobHandler.class);
TemporaryLobCreator lobCreator = mock(TemporaryLobCreator.class);
given(lobHandler.getLobCreator()).willReturn(lobCreator);
lobHandler.setCreateTemporaryLob(true);
this.repository.setLobHandler(lobHandler);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue");
this.repository.save(session);
verify(lobCreator, atLeastOnce()).close();
}
}

View File

@@ -1,17 +1,17 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.11.4'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.10.5.20201202'
}
dependencies {
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'com.maxmind.geoip2:geoip2:2.15.0'
dependency 'com.maxmind.geoip2:geoip2:2.3.1'
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2'
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.3'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.44.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.31'
dependency 'org.slf4j:log4j-over-slf4j:1.7.31'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.3'
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
dependency 'org.webjars:bootstrap:2.3.2'
dependency 'org.webjars:html5shiv:3.7.3'
dependency 'org.webjars:jquery:1.12.4'

View File

@@ -1,19 +0,0 @@
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"
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"
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2014-2020 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 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.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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
/**
* @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;
private WebDriver driver;
@BeforeEach
void setup() {
this.driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(this.mockMvc).build();
}
@AfterEach
void tearDown() {
this.driver.quit();
}
@Test
void home() {
LoginPage login = HomePage.go(this.driver);
login.assertAt();
}
@Test
void login() {
LoginPage login = HomePage.go(this.driver);
HomePage home = login.form().login(HomePage.class);
home.assertAt();
home.containCookie("SESSION");
home.doesNotContainCookie("JSESSIONID");
}
@Test
void logout() {
LoginPage login = HomePage.go(this.driver);
HomePage home = login.form().login(HomePage.class);
home.logout();
login.assertAt();
}
@TestConfiguration
static class Config {
@Bean
GenericContainer hazelcastContainer() {
GenericContainer hazelcastContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(5701);
hazelcastContainer.start();
return hazelcastContainer;
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import org.openqa.selenium.WebDriver;
/**
* @author Ellie Bahadori
*/
public class BasePage {
private WebDriver driver;
public BasePage(WebDriver driver) {
this.driver = driver;
}
public WebDriver getDriver() {
return this.driver;
}
public static void get(WebDriver driver, String get) {
String baseUrl = "http://localhost";
driver.get(baseUrl + get);
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import java.util.Set;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ellie Bahadori
*/
public class HomePage extends BasePage {
public HomePage(WebDriver driver) {
super(driver);
}
public static LoginPage go(WebDriver driver) {
get(driver, "/");
return PageFactory.initElements(driver, LoginPage.class);
}
public void assertAt() {
assertThat(getDriver().getTitle()).isEqualTo("Spring Session Sample - Secured Content");
}
public void containCookie(String cookieName) {
Set<Cookie> cookies = getDriver().manage().getCookies();
assertThat(cookies).extracting("name").contains(cookieName);
}
public void doesNotContainCookie(String cookieName) {
Set<Cookie> cookies = getDriver().manage().getCookies();
assertThat(cookies).extracting("name").doesNotContain(cookieName);
}
public HomePage logout() {
WebElement logout = getDriver().findElement(By.cssSelector("input[type=\"submit\"]"));
logout.click();
return PageFactory.initElements(getDriver(), HomePage.class);
}
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ellie Bahadori
*/
public class LoginPage extends BasePage {
public LoginPage(WebDriver driver) {
super(driver);
}
public void assertAt() {
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
}
public Form form() {
return new Form(getDriver());
}
public class Form {
@FindBy(name = "username")
private WebElement username;
@FindBy(name = "password")
private WebElement password;
@FindBy(tagName = "button")
private WebElement button;
public Form(SearchContext context) {
PageFactory.initElements(new DefaultElementLocatorFactory(context), this);
}
public <T> T login(Class<T> page) {
this.username.sendKeys("user");
this.password.sendKeys("password");
this.button.click();
return PageFactory.initElements(getDriver(), page);
}
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2014-2020 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 sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @author Ellie Bahadori
*/
@EnableCaching
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/")
public String index() {
return "index";
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Spring Security configuration.
*
* @author Ellie Bahadori
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// @formatter:off
// tag::config[]
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests((authorize) -> authorize
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
)
.formLogin((formLogin) -> formLogin
.permitAll()
);
}
// end::config[]
// @formatter:on
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
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;
// tag::class[]
@Configuration
public class SessionConfig {
@Bean
public Config clientConfig() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.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);
return config;
}
}
// end::class[]

View File

@@ -1 +0,0 @@
spring.security.user.password=password

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,11 +0,0 @@
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect" layout:decorate="~{layout}">
<head>
<title>Secured Content</title>
</head>
<body>
<div layout:fragment="content">
<h1>Secured Page</h1>
<p>This page is secured using Spring Boot, Spring Session, and Spring Security.</p>
</div>
</body>
</html>

View File

@@ -1,122 +0,0 @@
<!DOCTYPE html SYSTEM "https://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect">
<head>
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
<link rel="icon" type="image/x-icon" th:href="@{/favicon.ico}" href="../static/favicon.ico"/>
<link th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"></link>
<style type="text/css">
/* Sticky footer styles
-------------------------------------------------- */
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto !important;
height: 100%;
/* Negative indent footer by it's height */
margin: 0 auto -60px;
}
/* Set the fixed height of the footer here */
#push,
#footer {
height: 60px;
}
#footer {
background-color: #f5f5f5;
}
/* Lastly, apply responsive CSS fixes as necessary */
@media (max-width: 767px) {
#footer {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
.container {
width: auto;
max-width: 680px;
}
.container .credit {
margin: 20px 0;
text-align: center;
}
a {
color: green;
}
.navbar-form {
margin-left: 1em;
}
</style>
<link th:href="@{/webjars/bootstrap/css/bootstrap-responsive.min.css}" href="/webjars/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet"></link>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script th:src="@{/webjars/html5shiv/html5shiv.min.js}" src="/webjars/html5shiv/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div id="wrap">
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
sample_user
</p>
</div>
<ul class="nav">
</ul>
</div>
</div>
</div>
</div>
<!-- Begin page content -->
<div class="container">
<div class="alert alert-success"
th:if="${globalMessage}"
th:text="${globalMessage}">
Some Success message
</div>
<div layout:fragment="content">
Fake content
</div>
</div>
<div id="push"><!-- --></div>
</div>
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -1,20 +0,0 @@
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.0.3"
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"
}

View File

@@ -1,95 +0,0 @@
/*
* Copyright 2014-2020 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 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.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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class BootTests {
private static final String DOCKER_IMAGE = "hazelcast/hazelcast:latest";
@Autowired
private MockMvc mockMvc;
private WebDriver driver;
@BeforeEach
void setup() {
this.driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(this.mockMvc).build();
}
@AfterEach
void tearDown() {
this.driver.quit();
}
@Test
void home() {
LoginPage login = HomePage.go(this.driver);
login.assertAt();
}
@Test
void login() {
LoginPage login = HomePage.go(this.driver);
HomePage home = login.form().login(HomePage.class);
home.assertAt();
home.containCookie("SESSION");
home.doesNotContainCookie("JSESSIONID");
}
@Test
void logout() {
LoginPage login = HomePage.go(this.driver);
HomePage home = login.form().login(HomePage.class);
home.logout();
login.assertAt();
}
@TestConfiguration
static class Config {
@Bean
GenericContainer hazelcastContainer() {
GenericContainer hazelcastContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(5701);
hazelcastContainer.start();
return hazelcastContainer;
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import org.openqa.selenium.WebDriver;
public class BasePage {
private WebDriver driver;
public BasePage(WebDriver driver) {
this.driver = driver;
}
public WebDriver getDriver() {
return this.driver;
}
public static void get(WebDriver driver, String get) {
String baseUrl = "http://localhost";
driver.get(baseUrl + get);
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import java.util.Set;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import static org.assertj.core.api.Assertions.assertThat;
public class HomePage extends BasePage {
public HomePage(WebDriver driver) {
super(driver);
}
public static LoginPage go(WebDriver driver) {
get(driver, "/");
return PageFactory.initElements(driver, LoginPage.class);
}
public void assertAt() {
assertThat(getDriver().getTitle()).isEqualTo("Spring Session Sample - Secured Content");
}
public void containCookie(String cookieName) {
Set<Cookie> cookies = getDriver().manage().getCookies();
assertThat(cookies).extracting("name").contains(cookieName);
}
public void doesNotContainCookie(String cookieName) {
Set<Cookie> cookies = getDriver().manage().getCookies();
assertThat(cookies).extracting("name").doesNotContain(cookieName);
}
public HomePage logout() {
WebElement logout = getDriver().findElement(By.cssSelector("input[type=\"submit\"]"));
logout.click();
return PageFactory.initElements(getDriver(), HomePage.class);
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2014-2020 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 sample.pages;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import static org.assertj.core.api.Assertions.assertThat;
public class LoginPage extends BasePage {
public LoginPage(WebDriver driver) {
super(driver);
}
public void assertAt() {
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
}
public Form form() {
return new Form(getDriver());
}
public class Form {
@FindBy(name = "username")
private WebElement username;
@FindBy(name = "password")
private WebElement password;
@FindBy(tagName = "button")
private WebElement button;
public Form(SearchContext context) {
PageFactory.initElements(new DefaultElementLocatorFactory(context), this);
}
public <T> T login(Class<T> page) {
this.username.sendKeys("user");
this.password.sendKeys("password");
this.button.click();
return PageFactory.initElements(getDriver(), page);
}
}
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2014-2020 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 sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/")
public String index() {
return "index";
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// @formatter:off
// tag::config[]
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests((authorize) -> authorize
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
)
.formLogin((formLogin) -> formLogin
.permitAll()
);
}
// end::config[]
// @formatter:on
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2014-2020 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 sample.config;
import com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.IndexConfig;
import com.hazelcast.config.IndexType;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository;
import org.springframework.session.hazelcast.Hazelcast4PrincipalNameExtractor;
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
// tag::class[]
@Configuration
public class SessionConfig {
@Bean
public Config clientConfig() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
AttributeConfig attributeConfig = new AttributeConfig()
.setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);
return config;
}
}
// end::class[]

View File

@@ -1 +0,0 @@
spring.security.user.password=password

View File

@@ -1,11 +0,0 @@
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect" layout:decorate="~{layout}">
<head>
<title>Secured Content</title>
</head>
<body>
<div layout:fragment="content">
<h1>Secured Page</h1>
<p>This page is secured using Spring Boot, Spring Session, and Spring Security.</p>
</div>
</body>
</html>

View File

@@ -1,122 +0,0 @@
<!DOCTYPE html SYSTEM "https://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect">
<head>
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
<link rel="icon" type="image/x-icon" th:href="@{/favicon.ico}" href="../static/favicon.ico"/>
<link th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"></link>
<style type="text/css">
/* Sticky footer styles
-------------------------------------------------- */
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto !important;
height: 100%;
/* Negative indent footer by it's height */
margin: 0 auto -60px;
}
/* Set the fixed height of the footer here */
#push,
#footer {
height: 60px;
}
#footer {
background-color: #f5f5f5;
}
/* Lastly, apply responsive CSS fixes as necessary */
@media (max-width: 767px) {
#footer {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
.container {
width: auto;
max-width: 680px;
}
.container .credit {
margin: 20px 0;
text-align: center;
}
a {
color: green;
}
.navbar-form {
margin-left: 1em;
}
</style>
<link th:href="@{/webjars/bootstrap/css/bootstrap-responsive.min.css}" href="/webjars/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet"></link>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script th:src="@{/webjars/html5shiv/html5shiv.min.js}" src="/webjars/html5shiv/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div id="wrap">
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/images/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
sample_user
</p>
</div>
<ul class="nav">
</ul>
</div>
</div>
</div>
</div>
<!-- Begin page content -->
<div class="container">
<div class="alert alert-success"
th:if="${globalMessage}"
th:text="${globalMessage}">
Some Success message
</div>
<div layout:fragment="content">
Fake content
</div>
</div>
<div id="push"><!-- --></div>
</div>
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -115,7 +115,7 @@
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2020 the original author or authors.
* 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.
@@ -16,14 +16,8 @@
package sample.config;
import java.time.Duration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.session.RedisSessionProperties;
import org.springframework.boot.autoconfigure.session.SessionProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
@@ -31,22 +25,13 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisSessionRepository;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(RedisSessionProperties.class)
@EnableSpringHttpSession
public class SessionConfig {
private final SessionProperties sessionProperties;
private final RedisSessionProperties redisSessionProperties;
private final RedisConnectionFactory redisConnectionFactory;
public SessionConfig(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
this.sessionProperties = sessionProperties;
this.redisSessionProperties = redisSessionProperties;
this.redisConnectionFactory = redisConnectionFactory.getObject();
public SessionConfig(ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory.getIfAvailable();
}
@Bean
@@ -60,15 +45,7 @@ public class SessionConfig {
@Bean
public RedisSessionRepository sessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
RedisSessionRepository sessionRepository = new RedisSessionRepository(sessionRedisOperations);
Duration timeout = this.sessionProperties.getTimeout();
if (timeout != null) {
sessionRepository.setDefaultMaxInactiveInterval(timeout);
}
sessionRepository.setKeyNamespace(this.redisSessionProperties.getNamespace());
sessionRepository.setFlushMode(this.redisSessionProperties.getFlushMode());
sessionRepository.setSaveMode(this.redisSessionProperties.getSaveMode());
return sessionRepository;
return new RedisSessionRepository(sessionRedisOperations);
}
}

View File

@@ -121,7 +121,7 @@
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site
for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
for more <a href="https://github.com/spring-projects/spring-session/tree/master/samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -115,7 +115,7 @@
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class AttributeTests {
private static final String DOCKER_IMAGE = "redis:5.0.9";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@LocalServerPort
private int port;

View File

@@ -36,7 +36,7 @@ public class SessionController {
@GetMapping("/")
public String index(Model model, WebSession webSession) {
model.addAttribute("webSession", webSession);
return "home";
return "index";
}
}

View File

@@ -1,7 +1,5 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
ext['spring-data-bom.version'] = '2020.0.0'
dependencies {
compile project(':spring-session-data-redis')
compile "org.springframework.boot:spring-boot-starter-webflux"

View File

@@ -36,7 +36,7 @@ public class SessionController {
@GetMapping("/")
public String index(Model model, WebSession webSession) {
model.addAttribute("webSession", webSession);
return "home";
return "index";
}
private static final long serialVersionUID = 2878267318695777395L;

View File

@@ -8,7 +8,6 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile "org.springframework.boot:spring-boot-starter-data-redis"
compile "org.springframework.boot:spring-boot-starter-websocket"
compile "org.springframework.boot:spring-boot-starter-validation"
compile "org.springframework.boot:spring-boot-devtools"
compile "org.springframework:spring-websocket"
compile "org.springframework.security:spring-security-messaging"

View File

@@ -117,7 +117,7 @@
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
<p class="muted credit">Visit the <a href="https://projects.spring.io/spring-session/">Spring Session</a> site for more <a href="https://github.com/spring-projects/spring-session/tree/master/samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -2,7 +2,6 @@ apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile project(':spring-session-hazelcast')
compile project(':hazelcast4')
compile "org.springframework:spring-web"
compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-web"

View File

@@ -22,5 +22,4 @@ dependencies {
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
servletContainer = 'tomcat9'
}