Compare commits

...

107 Commits

Author SHA1 Message Date
Eleftheria Stein
1b4fb13d4d Release 2.4.4 2021-06-22 17:52:36 +02:00
Eleftheria Stein
21be7aac9c Update JDBC test images
Closes gh-1877
2021-06-22 17:37:00 +02:00
Eleftheria Stein
67f4850024 Revert "Release 2.4.4"
This reverts commit a92abcb26f.
2021-06-22 14:11:26 +02:00
Eleftheria Stein
a92abcb26f Release 2.4.4 2021-06-22 13:15:55 +02:00
Eleftheria Stein
041fb222f9 Revert "Use GPG_PRIVATE_KEY directly in build"
This reverts commit eed2ff195a.
2021-06-22 13:15:25 +02:00
Eleftheria Stein
dc951435d4 Upgrade test dependencies 2021-06-22 11:48:32 +02:00
Eleftheria Stein
e9243ceb59 Upgrade samples to Spring Boot 2.4.7
Closes gh-1876
2021-06-22 11:37:43 +02:00
Eleftheria Stein
8fb54ac1e0 Upgrade Spring Data to 2020.0.9
Closes gh-1875
2021-06-22 11:33:00 +02:00
Eleftheria Stein
b0558c1fd2 Upgrade Spring Framework to 5.3.8
Closes gh-1874
2021-06-22 11:31:44 +02:00
Eleftheria Stein
f570c2065b Upgrade Reactor to 2020.0.7
Closes gh-1873
2021-06-22 11:30:32 +02:00
Eleftheria Stein
eed2ff195a Use GPG_PRIVATE_KEY directly in build
Closes gh-1861
2021-05-31 16:47:34 +03:00
Eleftheria Stein
c467a7d656 Next development version 2021-04-13 13:39:30 +02:00
Eleftheria Stein
b9618fe6b3 Release 2.4.3 2021-04-13 13:21:46 +02:00
Eleftheria Stein
625833f1fe Upgrade test dependencies 2021-04-13 13:20:28 +02:00
Eleftheria Stein
f1eee1e7a4 Upgrade Spring Security to 5.4.6
Closes gh-1828
2021-04-13 13:19:39 +02:00
Eleftheria Stein
a2ee77c62e Upgrade Spring Data to 2020.0.7
Closes gh-1827
2021-04-13 13:18:11 +02:00
Eleftheria Stein
3c77d76cee Upgrade Spring Framework to 5.3.5
Closes gh-1826
2021-04-13 13:16:24 +02:00
Eleftheria Stein
6a8d2f4d88 Upgrade Reactor to 2020.0.6
Closes gh-1825
2021-04-13 13:15:39 +02:00
Eleftheria Stein
4c63faece1 Upgrade Hazelcast to 3.12.12
Closes gh-1824
2021-04-13 13:14:36 +02:00
Eleftheria Stein
e2a7e71ae3 Upgrade samples to Spring Boot 2.4.4
Closes gh-1823
2021-04-13 13:13:53 +02:00
Eleftheria Stein
66f81daa20 Throw exception if session created after response
Closes gh-1798
2021-03-25 12:01:53 +02:00
Eleftheria Stein
829b62bd19 Differentiate 2.4.x CI from master CI
- rename CI workflow file
- rename CI workflow identifier
- change daily build time
- ensure checkout is always run on 2.4.x

Issue: gh-1794
2021-02-22 11:19:13 +01:00
Eleftheria Stein
b7209f8330 Make Hazelcast tests independent of ordering
Closes gh-1787
2021-02-02 15:58:02 +01:00
Eleftheria Stein
645e525b84 Upgrade samples to Spring Boot 2.4.2
Closes gh-1782
2021-01-29 17:26:15 +01:00
Eleftheria Stein
ba4bb6fcf6 Use spring-build-conventions:0.0.37 2021-01-27 12:27:27 +01:00
Eleftheria Stein
f224d571f0 Next development version 2021-01-19 18:33:23 +01:00
Eleftheria Stein
3ba76d0375 Add manual trigger to CI workflow
Closes gh-1777
2021-01-19 17:41:19 +01:00
Eleftheria Stein
eb29949996 Release 2.4.2 2021-01-19 14:25:32 +01:00
Eleftheria Stein
02a990d00e Upgrade test dependencies 2021-01-18 13:21:12 +01:00
Eleftheria Stein
56809eacb2 Upgrade Hazelcast to 3.12.11
Closes gh-1766
2021-01-18 13:19:56 +01:00
Eleftheria Stein
4ff8f73b84 Upgrade Spring Data to 2020.0.3
Closes gh-1765
2021-01-18 13:17:38 +01:00
Eleftheria Stein
0ad389633e Upgrade Spring Security to 5.4.2
Closes gh-1764
2021-01-18 13:16:00 +01:00
Eleftheria Stein
5a9046e391 Upgrade Spring Framework to 5.3.3
Closes gh-1763
2021-01-18 13:15:21 +01:00
Eleftheria Stein
690f734307 Upgrade Reactor to 2020.0.3
Closes gh-1762
2021-01-18 13:14:06 +01:00
Eleftheria Stein
823e323f68 Add artifactory credentials to build 2020-11-18 14:39:48 +01:00
Eleftheria Stein
444b5ad85a Rename workflow name to match master 2020-11-10 13:40:22 +01:00
Eleftheria Stein
b4a8c7e516 Create GitHub Actions CI for 2.4.x 2020-11-10 11:52:45 +01:00
Eleftheria Stein
34876397a0 Next development version 2020-11-09 16:49:29 +01:00
Eleftheria Stein
faee8f1bdb Release 2.4.1 2020-11-09 15:40:43 +01:00
Eleftheria Stein
859784fe9e Use secrets from GitHub Actions workflow 2020-11-09 15:39:54 +01:00
Eleftheria Stein
4dd2db32d2 Revert "Release 2.4.1"
This reverts commit ae86831821.
2020-11-09 15:39:20 +01:00
Eleftheria Stein
ae86831821 Release 2.4.1 2020-11-04 17:36:47 +01:00
Eleftheria Stein
b722b12327 Fix formatting
Issue gh-1654
2020-10-30 14:34:37 +01:00
Kohei Tamura
29ff2e47fb Add try-with-resources to methods to insert BLOB 2020-10-30 08:45:52 -04:00
Eleftheria Stein
dc9da1d5bf Use OSSRH token credentials in workflow
Closes gh-1725
2020-10-30 13:42:36 +01:00
Eleftheria Stein
5a52df37ba Next development version 2020-10-28 23:36:40 +01:00
Eleftheria Stein
6d161575d5 Release 2.4.0wq 2020-10-28 22:48:46 +01:00
Eleftheria Stein
1cd8849eb9 Revert "Delete Jenkinsfile"
This reverts commit 68f867b60b.
2020-10-28 22:42:47 +01:00
Eleftheria Stein
cb3894614a Revert "Release 2.4.0"
This reverts commit 82e71d834b.
2020-10-28 22:42:06 +01:00
Eleftheria Stein
82e71d834b Release 2.4.0 2020-10-28 18:40:09 +01:00
Eleftheria Stein
81a9e71a5b Upgrade test and sample dependencies
This is needed in order for them to work with Spring Data 2020.0.0
2020-10-28 18:18:45 +01:00
Eleftheria Stein
298f0d59a0 Upgrade Spring Data to 2020.0.0
Closes gh-1721
2020-10-28 18:18:15 +01:00
Eleftheria Stein
c354284616 Upgrade samples to Spring Boot 2.4.0-M4
Closes gh-1722
2020-10-28 18:17:54 +01:00
Eleftheria Stein
4086044c2f Upgrade Spring Framework to 5.3.0
Closes gh-1720
2020-10-28 09:51:48 +01:00
Eleftheria Stein
e663401ecb Upgrade Hazelcast to 3.12.10
Closes gh-1718
2020-10-27 15:50:35 +01:00
Eleftheria Stein
60151c9e7d Upgrade Spring Security to 5.4.1
Closes gh-1717
2020-10-27 15:50:16 +01:00
Eleftheria Stein
18052460c6 Upgrade Reactor to 2020.0.0
Closes gh-1716
2020-10-27 15:49:49 +01:00
Eleftheria Stein
5092e86306 Upgrade samples to Spring Boot 2.3.4.RELEASE
Closes gh-1719
2020-10-27 15:49:19 +01:00
Eleftheria Stein
6de6df6dab Upgrade test dependencies 2020-10-27 15:15:30 +01:00
Vedran Pavic
301e65c2b9 Remove unnecessary Redis commands in RedisIndexedSessionRepository#save
See: #1331
2020-10-12 10:13:38 -04:00
Vedran Pavic
090a10fb10 Improve RedisSessionRepository-based sample configuration 2020-10-12 03:23:21 -04:00
Eleftheria Stein
235801487e Hazelcast4SessionUpdateEntryProcessor does not implement Offloadable
Closes gh-1707
2020-09-25 10:31:54 +02:00
Eleftheria Stein
e6e02de210 Upgrade Hazelcast 4 to 4.0.3
Closes gh-1706
2020-09-22 11:12:58 +02:00
Eleftheria Stein
b3b46fd8eb Upgrade Hazelcast to 3.12.9
Closes gh-1705
2020-09-22 10:46:15 +02:00
Eleftheria Stein
e46610f53a Next development version 2020-09-16 18:39:21 +02:00
Eleftheria Stein
e8c6b8db7b Release 2.4.0-RC1 2020-09-16 18:21:15 +02:00
Eleftheria Stein
486d00e5da Upgrade Spring Data to 2020.0.0-RC1
Closes gh-1704
2020-09-16 18:00:54 +02:00
Eleftheria Stein
0ab781e537 Consolidate Hazelcast configurations
Issue gh-1584
2020-09-16 16:35:03 +02:00
Eleftheria Stein
849b353cec Disable parallel deployment in CI build
Closes gh-1699
2020-09-16 09:59:38 +02:00
Eleftheria Stein
b262c9a3fd Upgrade Spring Framework to 5.3.0-RC1
Closes gh-1698
2020-09-15 17:18:35 +02:00
Eleftheria Stein
5d9e7caff0 Upgrade samples to Spring Boot 2.3.3.RELEASE
Closes gh-1683
2020-09-14 20:06:59 +02:00
Eleftheria Stein
dd348bc7b8 Upgrade test dependencies 2020-09-14 20:06:50 +02:00
Eleftheria Stein
9372986f84 Upgrade Spring Security to 5.4.0
Closes gh-1682
2020-09-14 19:41:49 +02:00
Eleftheria Stein
657c6a63e1 Upgrade Reactor to 2020.0.0-RC1
Closes gh-1681
2020-09-14 19:41:03 +02:00
Eleftheria Stein
a9c2336482 Use controller in Spring Boot sample
Issue gh-1647
2020-09-14 19:04:54 +02:00
Eleftheria Stein
068ed8d355 Ensure Hazelcast 4 compatibility with Java 9+ 2020-09-14 18:25:36 +02:00
Eleftheria Stein
2b6489c2bd Add support for Hazelcast 4
Closes gh-1584
2020-09-14 17:59:35 +02:00
Eleftheria Stein
c0c672b9f8 Update samples module link
Closes gh-1680
2020-09-09 17:04:37 +02:00
Ellie Bahadori
46d1205ff9 Create sample Spring Boot / Hazelcast project
Closes gh-1647
2020-09-09 15:44:31 +02:00
Enes Ozcan
cc85e927cd Add optional Hazelcast session serializer
Issue gh-1131
2020-09-08 07:31:32 -04:00
Ellie Bahadori
0819988a15 Move Gradle enterprise cache secrets to top level of CI build 2020-09-07 07:43:20 -04:00
Ellie Bahadori
0f3ea33b50 Fix indentation for cron job 2020-08-06 04:41:52 -04:00
Ellie Bahadori
0205c318d1 Remove placeholder comment from pipeline file 2020-08-04 05:18:40 -04:00
Ellie Bahadori
13bc1a5d24 Merge pull request #1663 from spring-projects/deploy-pipeline-test
Deploy pipeline test
2020-07-30 16:09:13 -07:00
Ellie Bahadori
8d2ec1ea44 Bring back master branch in preparation for merge
This reverts commit b54fb41952.
2020-07-30 14:53:18 -07:00
Ellie Bahadori
729ce13390 Add Gradle enterprise cache values to build steps 2020-07-30 14:34:37 -07:00
Ellie Bahadori
b54fb41952 Temporarily revert branch name changes to test artifact output
This reverts commit cf911322c2.
2020-07-30 14:10:10 -07:00
Ellie Bahadori
cf911322c2 Update badge to point to master and trigger builds on push to master branch 2020-07-28 11:46:38 -07:00
Ellie Bahadori
6bce5ddf7f Bump spring-build-conventions version and add README badge 2020-07-28 11:43:49 -07:00
Ellie Bahadori
7384504871 Fix YAML spacing issue 2020-07-27 12:14:12 -07:00
Ellie Bahadori
c21fff1a00 Add cron job back in 2020-07-27 11:57:36 -07:00
Ellie Bahadori
d602880a58 Re-introduce JDK matrix for CI pipeline 2020-07-27 11:00:01 -07:00
Ellie Bahadori
2a2c430793 Add URL for maven snapshots 2020-07-27 09:44:26 -07:00
Ellie Bahadori
6080611d1d Bump back up to 0.0.34.BUILD-SNAPSHOT 2020-07-27 09:44:26 -07:00
Ellie Bahadori
38adaeca94 Rev spring build conventions down to 0.0.33.RELEASE 2020-07-27 09:44:26 -07:00
Ellie Bahadori
6a791651e0 Bump spring build conventions version to 0.0.34.BUILD-SNAPSHOT 2020-07-27 09:44:26 -07:00
Ellie Bahadori
dfd6a0bc1b Add in deploy artifacts and docs steps 2020-07-27 09:44:26 -07:00
Ellie Bahadori
805820eeea Remove JDK version matrix for now 2020-07-27 09:44:26 -07:00
Ellie Bahadori
68f867b60b Delete Jenkinsfile 2020-07-27 09:44:26 -07:00
Ellie Bahadori
1044621caf Setup initial CI pipeline file 2020-07-27 09:44:26 -07:00
Eleftheria Stein-Kousathana
13f5cb4bac Document @SpringSessionDataSource in reference docs
Issue gh-1011
2020-07-27 12:14:41 +02:00
Thanh Nhan
5c05970b86 Update OncePerRequestFilter to match with spring-web
Closes gh-1658
2020-07-27 03:58:21 -04:00
Eleftheria Stein-Kousathana
0cd0bfb32f Remove attribute key and value from Redis
Closes gh-1331
2020-07-24 12:55:26 +02:00
Ellie Bahadori
b219806d8e Set up Github Actions pipeline for PRs 2020-07-23 04:01:58 -04:00
Eleftheria Stein
0f2a331ea3 Remove JDK 9 and 10 from Jenkins build
Closes gh-1659
2020-07-16 10:44:26 +02:00
Jay Bryant
ef8f667e35 Wording changes
Replacing some terms
2020-07-16 04:21:23 -04:00
Eleftheria Stein
4599e75c3a Next development version 2020-06-26 18:49:34 +02:00
98 changed files with 3771 additions and 331 deletions

View File

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

25
.github/workflows/pr-build-workflow.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: PR Build
on: pull_request
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2
- 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: ./gradlew clean build --no-daemon --stacktrace

View File

@@ -1,16 +0,0 @@
sudo: required
language: java
jdk:
- openjdk8
- openjdk11
services:
- docker
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
install: true
script: ./gradlew clean check --no-daemon --stacktrace

181
Jenkinsfile vendored
View File

@@ -1,181 +0,0 @@
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
pipelineTriggers([
cron('@daily')
]),
])
def SUCCESS = hudson.model.Result.SUCCESS.toString()
currentBuild.result = SUCCESS
try {
parallel check: {
stage('Check') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
label 'spring-session'
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew clean check --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: check'
throw e
}
finally {
junit '**/build/test-results/*/*.xml'
}
}
}
}
},
jdk9: {
stage('JDK 9') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk9'}"]) {
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk9'
throw e
}
}
}
}
},
jdk10: {
stage('JDK 10') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk10'}"]) {
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk10'
throw e
}
}
}
}
},
jdk11: {
stage('JDK 11') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk11'}"]) {
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk11'
throw e
}
}
}
}
},
jdk12: {
stage('JDK 12') {
timeout(time: 45, unit: 'MINUTES') {
node('linux') {
checkout scm
try {
withEnv(["JAVA_HOME=${tool 'openjdk12'}"]) {
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
currentBuild.result = 'FAILED: jdk12'
throw e
}
}
}
}
}
if (currentBuild.result == 'SUCCESS') {
parallel artifacts: {
stage('Deploy Artifacts') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withCredentials([file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')]) {
withCredentials([string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')]) {
withCredentials([usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')]) {
withCredentials([usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployArtifacts --no-daemon --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
sh './gradlew finalizeDeployArtifacts --no-daemon --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
}
}
}
}
}
}
catch (e) {
currentBuild.result = 'FAILED: artifacts'
throw e
}
}
}
},
docs: {
stage('Deploy Docs') {
node('linux') {
checkout scm
sh "git clean -dfx"
try {
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployDocs --no-daemon --stacktrace -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME'
}
}
}
catch (e) {
currentBuild.result = 'FAILED: docs'
throw e
}
}
}
}
}
}
finally {
def buildStatus = currentBuild.result
def buildNotSuccess = !SUCCESS.equals(buildStatus)
def lastBuildNotSuccess = !SUCCESS.equals(currentBuild.previousBuild?.result)
if (buildNotSuccess || lastBuildNotSuccess) {
stage('Notify') {
node {
final def RECIPIENTS = [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']]
def subject = "${buildStatus}: Build ${env.JOB_NAME} ${env.BUILD_NUMBER} status is now ${buildStatus}"
def details = "The build status changed to ${buildStatus}. For details see ${env.BUILD_URL}"
emailext(
subject: subject,
body: details,
recipientProviders: RECIPIENTS,
to: "$SPRING_SESSION_TEAM_EMAILS"
)
}
}
}
}

View File

@@ -1,6 +1,8 @@
= Spring Session
image:https://travis-ci.org/spring-projects/spring-session.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/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"]
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"]
image:https://github.com/spring-projects/spring-session/workflows/CI/badge.svg?branch=master["Build Status", link="https://github.com/spring-projects/spring-session/actions?query=workflow%3ACI"]
Spring Session provides an API and implementations for managing a user's session information, while also making it trivial to support clustered sessions without being tied to an application container specific solution.
It also provides transparent integration with:

View File

@@ -4,16 +4,17 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.3.1.RELEASE'
springBootVersion = '2.4.7'
}
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release/' }
maven { url 'https://repo.spring.io/plugins-snapshot' }
}
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.32.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.37'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}

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.0-M1
version=2.4.4

View File

@@ -1,35 +1,36 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-SR9'
mavenBom 'org.junit:junit-bom:5.6.2'
mavenBom 'org.springframework:spring-framework-bom:5.3.0-M1'
mavenBom 'org.springframework.data:spring-data-bom:2020.0.0-M1'
mavenBom 'org.springframework.security:spring-security-bom:5.3.3.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.14.3'
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'
}
dependencies {
dependencySet(group: 'com.hazelcast', version: '3.12.7') {
dependencySet(group: 'com.hazelcast', version: '3.12.12') {
entry 'hazelcast'
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.6.0.0'
dependency 'com.oracle.database.jdbc:ojdbc8:19.8.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.3.1.RELEASE'
dependency 'io.lettuce:lettuce-core:6.0.5.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:4.0.1'
dependency 'junit:junit:4.13'
dependency 'mysql:mysql-connector-java:8.0.20'
dependency 'junit:junit:4.13.2'
dependency 'mysql:mysql-connector-java:8.0.23'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.16.1'
dependency 'org.hsqldb:hsqldb:2.5.0'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.6.1'
dependency 'org.mockito:mockito-core:3.3.3'
dependency 'org.postgresql:postgresql:42.2.14'
dependency 'org.assertj:assertj-core:3.18.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'
}
}

View File

@@ -5,6 +5,8 @@ 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,5 +25,6 @@ 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 = this.alreadyFilteredAttributeName;
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {

View File

@@ -309,6 +309,10 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
if (!create) {
return null;
}
if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
&& this.response.isCommitted()) {
throw new IllegalStateException("Cannot create a session after the response has been committed");
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "

View File

@@ -0,0 +1,70 @@
/*
* 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

@@ -62,6 +62,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -423,6 +424,18 @@ class SessionRepositoryFilterTests {
assertThat(this.response.getCookie("SESSION")).isNotNull();
}
@Test
void doFilterGetSessionNewWhenResponseCommittedThenException() {
assertThatIllegalStateException().isThrownBy(() -> doFilter(new DoInFilter() {
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse)
throws IOException {
wrappedResponse.getWriter().flush();
wrappedRequest.getSession();
}
}));
}
@Test
void doFilterGetSessionNew() throws Exception {
doFilter(new DoInFilter() {

View File

@@ -29,7 +29,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
protected static class BaseConfig {

View File

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

View File

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

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 terminate the session from the library using clues like the location, created time, last accessed time, and so on.
* User can log in and end 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_
* Terminate your original session.
* End 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 terminate any WebSocket connections that are still open when a Spring Session is terminated.
The `WebSocketSession` is necessary to end any WebSocket connections that are still open when a Spring Session is ended.
* `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 terminated.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is ended.
// 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 terminated.
This means you need to wait at most two minutes before the WebSocket connection is closed.
====
You can now try accessing http://localhost:8080/

View File

@@ -97,11 +97,18 @@ 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> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
<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.
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,6 +128,36 @@ 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,6 +58,10 @@ 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]
@@ -1177,8 +1181,8 @@ include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schem
==== Transaction Management
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).
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).
[[api-hazelcastindexedsessionrepository]]
=== Using `HazelcastIndexedSessionRepository`

View File

@@ -19,12 +19,15 @@ 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;
@@ -42,7 +45,10 @@ public class HazelcastHttpSessionConfig {
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config); // <3>
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
return Hazelcast.newHazelcastInstance(config); // <4>
}
}

View File

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,201 @@
/*
* 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

@@ -0,0 +1,78 @@
/*
* 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

@@ -0,0 +1,51 @@
/*
* 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

@@ -0,0 +1,61 @@
/*
* 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

@@ -0,0 +1,218 @@
/*
* 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

@@ -0,0 +1,72 @@
/*
* 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

@@ -0,0 +1,17 @@
<?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

@@ -0,0 +1 @@
ryuk.container.timeout=120

View File

@@ -0,0 +1,486 @@
/*
* 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

@@ -0,0 +1,43 @@
/*
* 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

@@ -0,0 +1,88 @@
/*
* 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

@@ -0,0 +1,472 @@
/*
* 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,17 +1,29 @@
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-2019 the original author or authors.
* Copyright 2014-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,16 +58,13 @@ 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.size()).isEqualTo(0);
assertThat(hazelcastMap.get(sessionId)).isNull();
}
@Test
@@ -183,6 +180,8 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
@@ -199,6 +198,8 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
@@ -220,6 +221,8 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
assertThat(this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username))
.hasSize(1);
this.repository.deleteById(session.getId());
}
}

View File

@@ -46,7 +46,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
@WebAppConfiguration
class ClientServerHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.3")
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.12")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml");

View File

@@ -20,9 +20,12 @@ 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.
*
@@ -48,6 +51,9 @@ 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

@@ -0,0 +1,159 @@
/*
* 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,7 +33,6 @@ 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;
/**
@@ -81,11 +80,10 @@ public @interface EnableHazelcastHttpSession {
/**
* This is the name of the Map that will be used in Hazelcast to store the session
* data. Default is
* {@link HazelcastIndexedSessionRepository#DEFAULT_SESSION_MAP_NAME}.
* data. Default is "spring:session:sessions".
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
String sessionMapName() default "spring:session:sessions";
/**
* Flush mode for the Hazelcast sessions. The default is {@code ON_SAVE} which only

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* 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.
@@ -35,12 +35,15 @@ 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;
/**
@@ -72,23 +75,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 HazelcastIndexedSessionRepository sessionRepository() {
HazelcastIndexedSessionRepository sessionRepository = new HazelcastIndexedSessionRepository(
this.hazelcastInstance);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
public SessionRepository<?> sessionRepository() {
if (hazelcast4) {
return createHazelcast4IndexedSessionRepository();
}
if (StringUtils.hasText(this.sessionMapName)) {
sessionRepository.setSessionMapName(this.sessionMapName);
else {
return createHazelcastIndexedSessionRepository();
}
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) {
@@ -139,6 +142,13 @@ 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) {
@@ -159,4 +169,40 @@ 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

@@ -0,0 +1,99 @@
/*
* 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

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

View File

@@ -44,6 +44,7 @@ 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;
@@ -460,63 +461,65 @@ public class JdbcIndexedSessionRepository
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
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);
@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);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
serialize(session.getAttribute(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);
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");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
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);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
serialize(session.getAttribute(attributeName)));
@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)));
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,6 +35,8 @@ 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;
@@ -53,6 +55,8 @@ 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;
@@ -710,4 +714,19 @@ 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,23 +1,23 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.11.0'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.11.4'
}
dependencies {
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'com.maxmind.geoip2:geoip2:2.14.0'
dependency 'com.maxmind.geoip2:geoip2:2.15.0'
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2'
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.3'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.36.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
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.webjars:bootstrap:2.3.2'
dependency 'org.webjars:html5shiv:3.7.3'
dependency 'org.webjars:jquery:1.12.4'
dependency 'org.webjars:knockout:2.3.0'
dependency 'org.webjars:sockjs-client:0.3.4'
dependency 'org.webjars:stomp-websocket:2.3.3'
dependency 'org.webjars:stomp-websocket:2.3.4'
dependency 'org.webjars:webjars-taglib:0.3'
}
}

View File

@@ -46,7 +46,7 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class FindByUsernameTests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Autowired
private MockMvc mockMvc;

View File

@@ -0,0 +1,19 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-session-hazelcast')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast-client"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator-core"
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

@@ -0,0 +1,98 @@
/*
* 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

@@ -0,0 +1,41 @@
/*
* 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

@@ -0,0 +1,63 @@
/*
* 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

@@ -0,0 +1,69 @@
/*
* 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

@@ -0,0 +1,34 @@
/*
* 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

@@ -0,0 +1,30 @@
/*
* 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

@@ -0,0 +1,48 @@
/*
* 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

@@ -0,0 +1,56 @@
/*
* 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<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

@@ -0,0 +1,122 @@
<!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

@@ -0,0 +1,20 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-session-hazelcast')
compile project(':hazelcast4')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "com.hazelcast:hazelcast:4.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

@@ -0,0 +1,95 @@
/*
* 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

@@ -0,0 +1,38 @@
/*
* 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

@@ -0,0 +1,60 @@
/*
* 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

@@ -0,0 +1,66 @@
/*
* 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

@@ -0,0 +1,31 @@
/*
* 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

@@ -0,0 +1,30 @@
/*
* 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

@@ -0,0 +1,43 @@
/*
* 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

@@ -0,0 +1,57 @@
/*
* 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<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

@@ -0,0 +1,122 @@
<!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/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/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -50,7 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureMockMvc
class HttpRedisJsonTest {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Autowired
private MockMvc mockMvc;

View File

@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class RedisSerializerTest {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@SpringSessionRedisOperations
private RedisTemplate<Object, Object> sessionRedisTemplate;

View File

@@ -41,7 +41,7 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class BootTests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Autowired
private MockMvc mockMvc;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* 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.
@@ -16,8 +16,14 @@
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;
@@ -25,13 +31,22 @@ 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(ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory.getIfAvailable();
public SessionConfig(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
this.sessionProperties = sessionProperties;
this.redisSessionProperties = redisSessionProperties;
this.redisConnectionFactory = redisConnectionFactory.getObject();
}
@Bean
@@ -45,7 +60,15 @@ public class SessionConfig {
@Bean
public RedisSessionRepository sessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
return new RedisSessionRepository(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;
}
}

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/samples">samples</a>.</p>
for more <a href="https://github.com/spring-projects/spring-session/tree/master/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -45,7 +45,7 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
class BootTests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Autowired
private MockMvc mockMvc;

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/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/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -1,5 +1,7 @@
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

@@ -47,7 +47,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.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@LocalServerPort
private int port;

View File

@@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationTests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Value("${local.server.port}")
private String port;

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/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/spring-session-samples">samples</a>.</p>
</div>
</div>
</body>

View File

@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Bean
public GenericContainer redisContainer() {

View File

@@ -2,6 +2,7 @@ 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

@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Bean
public GenericContainer redisContainer() {

View File

@@ -53,7 +53,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebAppConfiguration
class RestMockMvcTests {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Autowired
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;

View File

@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Bean
public GenericContainer redisContainer() {

View File

@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Bean
public GenericContainer redisContainer() {

View File

@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:5.0.6";
private static final String DOCKER_IMAGE = "redis:5.0.10";
@Bean
public GenericContainer redisContainer() {