Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba76d0375 | ||
|
|
eb29949996 | ||
|
|
02a990d00e | ||
|
|
56809eacb2 | ||
|
|
4ff8f73b84 | ||
|
|
0ad389633e | ||
|
|
5a9046e391 | ||
|
|
690f734307 | ||
|
|
823e323f68 | ||
|
|
444b5ad85a | ||
|
|
b4a8c7e516 | ||
|
|
34876397a0 | ||
|
|
faee8f1bdb | ||
|
|
859784fe9e | ||
|
|
4dd2db32d2 | ||
|
|
ae86831821 | ||
|
|
b722b12327 | ||
|
|
29ff2e47fb | ||
|
|
dc9da1d5bf | ||
|
|
5a52df37ba | ||
|
|
6d161575d5 | ||
|
|
1cd8849eb9 | ||
|
|
cb3894614a | ||
|
|
82e71d834b | ||
|
|
81a9e71a5b | ||
|
|
298f0d59a0 | ||
|
|
c354284616 | ||
|
|
4086044c2f | ||
|
|
e663401ecb | ||
|
|
60151c9e7d | ||
|
|
18052460c6 | ||
|
|
5092e86306 | ||
|
|
6de6df6dab | ||
|
|
301e65c2b9 | ||
|
|
090a10fb10 | ||
|
|
235801487e | ||
|
|
e6e02de210 | ||
|
|
b3b46fd8eb | ||
|
|
e46610f53a | ||
|
|
e8c6b8db7b | ||
|
|
486d00e5da | ||
|
|
0ab781e537 | ||
|
|
849b353cec | ||
|
|
b262c9a3fd | ||
|
|
5d9e7caff0 | ||
|
|
dd348bc7b8 | ||
|
|
9372986f84 | ||
|
|
657c6a63e1 | ||
|
|
a9c2336482 | ||
|
|
068ed8d355 | ||
|
|
2b6489c2bd | ||
|
|
c0c672b9f8 | ||
|
|
46d1205ff9 | ||
|
|
cc85e927cd | ||
|
|
0819988a15 | ||
|
|
0f3ea33b50 | ||
|
|
0205c318d1 | ||
|
|
13bc1a5d24 | ||
|
|
8d2ec1ea44 | ||
|
|
729ce13390 | ||
|
|
b54fb41952 | ||
|
|
cf911322c2 | ||
|
|
6bce5ddf7f | ||
|
|
7384504871 | ||
|
|
c21fff1a00 | ||
|
|
d602880a58 | ||
|
|
2a2c430793 | ||
|
|
6080611d1d | ||
|
|
38adaeca94 | ||
|
|
6a791651e0 | ||
|
|
dfd6a0bc1b | ||
|
|
805820eeea | ||
|
|
68f867b60b | ||
|
|
1044621caf | ||
|
|
13f5cb4bac | ||
|
|
5c05970b86 | ||
|
|
0cd0bfb32f | ||
|
|
b219806d8e | ||
|
|
0f2a331ea3 | ||
|
|
ef8f667e35 | ||
|
|
4599e75c3a | ||
|
|
8a971b9ce1 | ||
|
|
56e9dcfe20 | ||
|
|
59e2cdb74f | ||
|
|
847433562e | ||
|
|
55a6967331 | ||
|
|
2c8ce67ffc | ||
|
|
076ed5cd71 | ||
|
|
f1ea71e55e | ||
|
|
5acb307a54 | ||
|
|
f921c4f527 | ||
|
|
12dc76ec36 | ||
|
|
7be3d30981 | ||
|
|
9c8fe23789 | ||
|
|
3114ef51ec | ||
|
|
9e7736bf7f | ||
|
|
6c5e335568 | ||
|
|
1deedad3b9 | ||
|
|
e4a8a6aa5c | ||
|
|
49375a28fa | ||
|
|
5375f51bca | ||
|
|
29af9d3a4d | ||
|
|
997ff56c63 | ||
|
|
06d8031211 | ||
|
|
904369ac29 | ||
|
|
266854a0be | ||
|
|
8f02c83e06 | ||
|
|
570a7686b1 | ||
|
|
fed318abc7 | ||
|
|
a824edd1c3 | ||
|
|
aa4f783b45 | ||
|
|
11fb68444f | ||
|
|
00026a30f4 | ||
|
|
c007437bd3 | ||
|
|
dda13b5619 | ||
|
|
366f13bd25 | ||
|
|
3535137c47 | ||
|
|
a9bca9088f | ||
|
|
31de86ecef | ||
|
|
d123960f89 | ||
|
|
16d2923efd | ||
|
|
24015d0854 | ||
|
|
d8f160c178 | ||
|
|
0318f6e2c1 | ||
|
|
43dd571345 | ||
|
|
e7fb9fce47 | ||
|
|
f13eb8d73e | ||
|
|
1a07ba5114 | ||
|
|
7125aac567 | ||
|
|
3cbd3a9e25 | ||
|
|
4c914d46c9 | ||
|
|
adf411ecc3 | ||
|
|
95b39a203f | ||
|
|
3d653b3b50 | ||
|
|
938fd3c2e5 | ||
|
|
45bb0f9b0c | ||
|
|
cddd84d564 | ||
|
|
6931d40e6e | ||
|
|
3b672787f3 | ||
|
|
c0ee52b33b | ||
|
|
68f8641233 | ||
|
|
e7b2af47e1 | ||
|
|
1ad6cbd7f8 | ||
|
|
195af52d0b | ||
|
|
bc9d5f1299 | ||
|
|
3a4345eb6a | ||
|
|
6c41dea893 | ||
|
|
ee1d5b3b3c | ||
|
|
89a4255679 | ||
|
|
6d2e51a0b9 | ||
|
|
798d398d9b | ||
|
|
085554f56b | ||
|
|
45b3b35db7 | ||
|
|
2d06e1159c | ||
|
|
927008bdc8 | ||
|
|
30588dc3c8 | ||
|
|
2f79da00dc | ||
|
|
e2abe36fa8 | ||
|
|
456fd3adb4 | ||
|
|
bd0f474b5b | ||
|
|
e5a3933cb6 | ||
|
|
71e5cc857a | ||
|
|
df455ddc89 | ||
|
|
eceeaa665d | ||
|
|
e6c54d8a75 | ||
|
|
c88456a183 | ||
|
|
f5abd55394 | ||
|
|
b9fd3666b5 | ||
|
|
e06ea36ad5 | ||
|
|
0a1701233e | ||
|
|
47a4873199 | ||
|
|
bd36e115a8 | ||
|
|
ec82336477 | ||
|
|
feaf8780a8 | ||
|
|
b357a76ce3 | ||
|
|
2c6f22afb0 | ||
|
|
34306fd3a0 | ||
|
|
a6c1d8eb1d | ||
|
|
e48b46a2d5 | ||
|
|
8cc8fbb7fd | ||
|
|
96715e04f2 | ||
|
|
121a633a40 | ||
|
|
bf31a9b04b | ||
|
|
a209d436d1 | ||
|
|
6c76a1ccdd | ||
|
|
c974eeb188 | ||
|
|
3b5dadb07f | ||
|
|
3e6b3fda0f | ||
|
|
840da7fb5a | ||
|
|
560ee5ff4f | ||
|
|
072348e28f | ||
|
|
99dfdda7b7 | ||
|
|
18b097d9c7 | ||
|
|
702a35fac6 | ||
|
|
df3e4c5bc1 | ||
|
|
f746233255 | ||
|
|
f6c82f1eee | ||
|
|
bcdd05a0bc | ||
|
|
5d26ab4df4 | ||
|
|
e55d86f5e2 | ||
|
|
fe480b338c | ||
|
|
4b13392430 | ||
|
|
e5d9ce6ead | ||
|
|
bc1ef4359a |
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,7 +0,0 @@
|
||||
<!--
|
||||
For Security Vulnerabilities, please use https://pivotal.io/security#reporting
|
||||
-->
|
||||
|
||||
<!--
|
||||
Thanks for raising a Spring Session issue. Please provide a brief description of your problem along with the version of Spring Session that you are using. If possible, please also consider putting together a sample application that reproduces the issue.
|
||||
-->
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'type: bug, status: waiting-for-triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Sample**
|
||||
|
||||
A link to a GitHub repository with a [minimal, reproducible sample](https://stackoverflow.com/help/minimal-reproducible-example).
|
||||
|
||||
Reports that include a sample will take priority over reports that do not.
|
||||
At times, we may require a sample, so it is good to try and include a sample up front.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Support
|
||||
url: https://stackoverflow.com/questions/tagged/spring-security
|
||||
about: Please ask and answer questions on StackOverflow with the tag spring-session
|
||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'status: waiting-for-triage, type: enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
<!--- Tell us how it should work -->
|
||||
|
||||
**Current Behavior**
|
||||
|
||||
<!--- Explain the difference from current behavior -->
|
||||
|
||||
**Context**
|
||||
|
||||
<!---
|
||||
How has this issue affected you?
|
||||
What are you trying to accomplish?
|
||||
What other alternatives have you considered?
|
||||
Are you aware of any workarounds?
|
||||
-->
|
||||
90
.github/workflows/continuous-integration-workflow.yml
vendored
Normal file
90
.github/workflows/continuous-integration-workflow.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 2.4.x
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
25
.github/workflows/pr-build-workflow.yml
vendored
Normal 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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ out
|
||||
.checkstyle
|
||||
!etc/eclipse/.checkstyle
|
||||
!**/src/**/build
|
||||
.DS_Store
|
||||
|
||||
20
.travis.yml
20
.travis.yml
@@ -1,20 +0,0 @@
|
||||
language: java
|
||||
|
||||
sudo: required
|
||||
|
||||
services: docker
|
||||
|
||||
jdk: oraclejdk8
|
||||
|
||||
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 build --refresh-dependencies --no-daemon
|
||||
@@ -1,44 +0,0 @@
|
||||
= Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, and in the interest of fostering an open
|
||||
and welcoming community, we pledge to respect all people who contribute through reporting
|
||||
issues, posting feature requests, updating documentation, submitting pull requests or
|
||||
patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for
|
||||
everyone, regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
* Other unethical or unprofessional conduct
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments,
|
||||
commits, code, wiki edits, issues, and other contributions that are not aligned to this
|
||||
Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors
|
||||
that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
|
||||
consistently applying these principles to every aspect of managing this project. Project
|
||||
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
|
||||
from the project team.
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an
|
||||
individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||
contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
|
||||
be reviewed and investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
|
||||
with regard to the reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the
|
||||
https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at
|
||||
https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]
|
||||
@@ -3,9 +3,16 @@
|
||||
Spring Session is released under the Apache 2.0 license. If you would like to contribute
|
||||
something, or simply want to hack on the code this document should help you get started.
|
||||
|
||||
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
Please see our https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md[code of conduct]
|
||||
|
||||
|
||||
== Reporting Security Vulnerabilities
|
||||
|
||||
Please see our https://github.com/spring-projects/spring-session/security/policy[Security policy].
|
||||
|
||||
|
||||
== Using GitHub issues
|
||||
|
||||
|
||||
180
Jenkinsfile
vendored
180
Jenkinsfile
vendored
@@ -1,180 +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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 --refresh-dependencies --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 finalizeDeployArtifacts --no-daemon --refresh-dependencies --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 --refresh-dependencies --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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
README.adoc
14
README.adoc
@@ -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:
|
||||
@@ -18,10 +20,16 @@ Spring Session consists of the following modules:
|
||||
* Spring Session JDBC - provides `SessionRepository` implementation backed by a relational database and configuration support
|
||||
* Spring Session Hazelcast - provides `SessionRepository` implementation backed by Hazelcast and configuration support
|
||||
|
||||
|
||||
== Code of Conduct
|
||||
|
||||
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
Please see our https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md[code of conduct]
|
||||
|
||||
|
||||
== Reporting Security Vulnerabilities
|
||||
|
||||
Please see our https://github.com/spring-projects/spring-session/security/policy[Security policy].
|
||||
|
||||
|
||||
== Spring Session Project Site
|
||||
|
||||
|
||||
11
build.gradle
11
build.gradle
@@ -4,16 +4,17 @@ buildscript {
|
||||
snapshotBuild = version.endsWith('SNAPSHOT')
|
||||
milestoneBuild = !(releaseBuild || snapshotBuild)
|
||||
|
||||
springBootVersion = '2.2.0.M4'
|
||||
springBootVersion = '2.4.0-M4'
|
||||
}
|
||||
|
||||
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.26.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.35.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
}
|
||||
@@ -28,9 +29,9 @@ subprojects {
|
||||
|
||||
plugins.withType(JavaPlugin) {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
<suppress files="[\\/]spring-session-docs[\\/]" checks="InnerTypeLast"/>
|
||||
<suppress files="[\\/]spring-session-samples[\\/]" checks="Javadoc*"/>
|
||||
<suppress files="[\\/]spring-session-samples[\\/].+Application\.java" checks="HideUtilityClassConstructor"/>
|
||||
<suppress files="SessionRepositoryFilterTests\.java" checks="SpringLambda"/>
|
||||
<suppress files="CookieSerializer\.java" checks="SpringMethodVisibility"/>
|
||||
</suppressions>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
version=2.2.0.M3
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
version=2.4.2
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-M3'
|
||||
mavenBom 'org.junit:junit-bom:5.5.1'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RC1'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-RC2'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.M4'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.12.0'
|
||||
mavenBom 'io.projectreactor:reactor-bom:2020.0.3'
|
||||
mavenBom 'org.junit:junit-bom:5.7.0'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.3.3'
|
||||
mavenBom 'org.springframework.data:spring-data-bom:2020.0.3'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.4.2'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.15.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.12.2') {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.12.11') {
|
||||
entry 'hazelcast'
|
||||
entry 'hazelcast-client'
|
||||
}
|
||||
|
||||
dependency 'com.h2database:h2:1.4.199'
|
||||
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.zaxxer:HikariCP:3.3.1'
|
||||
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.1.8.RELEASE'
|
||||
dependency 'io.lettuce:lettuce-core:6.0.2.RELEASE'
|
||||
dependency 'javax.annotation:javax.annotation-api:1.3.2'
|
||||
dependency 'javax.servlet:javax.servlet-api:4.0.1'
|
||||
dependency 'mysql:mysql-connector-java:8.0.17'
|
||||
dependency 'junit:junit:4.13.1'
|
||||
dependency 'mysql:mysql-connector-java:8.0.22'
|
||||
dependency 'org.apache.derby:derby:10.14.2.0'
|
||||
dependency 'org.assertj:assertj-core:3.13.2'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.0'
|
||||
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.3'
|
||||
dependency 'org.mockito:mockito-core:3.0.0'
|
||||
dependency 'org.postgresql:postgresql:42.2.6'
|
||||
dependency 'org.assertj:assertj-core:3.18.0'
|
||||
dependency 'org.hsqldb:hsqldb:2.5.1'
|
||||
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.0'
|
||||
dependency 'org.mockito:mockito-core:3.5.15'
|
||||
dependency 'org.postgresql:postgresql:42.2.18'
|
||||
}
|
||||
}
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
4
gradlew
vendored
4
gradlew
vendored
@@ -125,8 +125,8 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexRe
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
private static final Expression expression = new SpelExpressionParser().parseExpression("authentication?.name");
|
||||
|
||||
public PrincipalNameIndexResolver() {
|
||||
super(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
|
||||
@@ -45,7 +45,6 @@ public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexRe
|
||||
}
|
||||
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
|
||||
if (authentication != null) {
|
||||
Expression expression = parser.parseExpression("authentication?.name");
|
||||
return expression.getValue(authentication, String.class);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.config;
|
||||
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
|
||||
/**
|
||||
* Strategy that can be used to customize the {@link ReactiveSessionRepository} before it
|
||||
* is fully initialized, in particular to tune its configuration.
|
||||
*
|
||||
* @param <T> the {@link ReactiveSessionRepository} type
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveSessionRepositoryCustomizer<T extends ReactiveSessionRepository> {
|
||||
|
||||
/**
|
||||
* Customize the {@link ReactiveSessionRepository}.
|
||||
* @param sessionRepository the {@link ReactiveSessionRepository} to customize
|
||||
*/
|
||||
void customize(T sessionRepository);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.config;
|
||||
|
||||
import org.springframework.session.SessionRepository;
|
||||
|
||||
/**
|
||||
* Strategy that can be used to customize the {@link SessionRepository} before it is fully
|
||||
* initialized, in particular to tune its configuration.
|
||||
*
|
||||
* @param <T> the {@link SessionRepository} type
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SessionRepositoryCustomizer<T extends SessionRepository> {
|
||||
|
||||
/**
|
||||
* Customize the {@link SessionRepository}.
|
||||
* @param sessionRepository the {@link SessionRepository} to customize
|
||||
*/
|
||||
void customize(T sessionRepository);
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
|
||||
*
|
||||
* {@literal @Bean}
|
||||
* public MapSessionRepository sessionRepository() {
|
||||
* return new MapSessionRepository();
|
||||
* return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
* }
|
||||
*
|
||||
* }
|
||||
@@ -74,7 +74,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
|
||||
@Target({ java.lang.annotation.ElementType.TYPE })
|
||||
@Documented
|
||||
@Import(SpringHttpSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableSpringHttpSession {
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ import org.springframework.util.ObjectUtils;
|
||||
*
|
||||
* {@literal @Bean}
|
||||
* public MapSessionRepository sessionRepository() {
|
||||
* return new MapSessionRepository();
|
||||
* return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
* }
|
||||
*
|
||||
* }
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.context.annotation.Import;
|
||||
*
|
||||
* {@literal @Bean}
|
||||
* public ReactiveSessionRepository sessionRepository() {
|
||||
* return new ReactiveMapSessionRepository();
|
||||
* return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
|
||||
* }
|
||||
*
|
||||
* }
|
||||
@@ -49,7 +49,7 @@ import org.springframework.context.annotation.Import;
|
||||
@Target({ java.lang.annotation.ElementType.TYPE })
|
||||
@Documented
|
||||
@Import(SpringWebSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableSpringWebSession {
|
||||
|
||||
}
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
|
||||
package org.springframework.session.security;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -110,13 +109,8 @@ public class SpringSessionBackedSessionRegistry<S extends Session> implements Se
|
||||
* could be derived
|
||||
*/
|
||||
protected String name(Object principal) {
|
||||
if (principal instanceof UserDetails) {
|
||||
return ((UserDetails) principal).getUsername();
|
||||
}
|
||||
if (principal instanceof Principal) {
|
||||
return ((Principal) principal).getName();
|
||||
}
|
||||
return principal.toString();
|
||||
// We are reusing the logic from AbstractAuthenticationToken#getName
|
||||
return new TestingAuthenticationToken(principal, null).getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
package org.springframework.session.web.http;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
@@ -62,6 +63,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
domainValid.set('-');
|
||||
}
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
private String cookieName = "SESSION";
|
||||
|
||||
private Boolean useSecureCookie;
|
||||
@@ -121,7 +124,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
public void writeCookieValue(CookieValue cookieValue) {
|
||||
HttpServletRequest request = cookieValue.getRequest();
|
||||
HttpServletResponse response = cookieValue.getResponse();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(this.cookieName).append('=');
|
||||
String value = getValue(cookieValue);
|
||||
@@ -132,8 +134,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
int maxAge = getMaxAge(cookieValue);
|
||||
if (maxAge > -1) {
|
||||
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
|
||||
OffsetDateTime expires = (maxAge != 0) ? OffsetDateTime.now().plusSeconds(maxAge)
|
||||
: Instant.EPOCH.atOffset(ZoneOffset.UTC);
|
||||
ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
|
||||
: Instant.EPOCH.atZone(ZoneOffset.UTC);
|
||||
sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
String domain = getDomainName(request);
|
||||
@@ -155,7 +157,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
if (this.sameSite != null) {
|
||||
sb.append("; SameSite=").append(this.sameSite);
|
||||
}
|
||||
|
||||
response.addHeader("Set-Cookie", sb.toString());
|
||||
}
|
||||
|
||||
@@ -259,6 +260,10 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
void setClock(Clock clock) {
|
||||
this.clock = clock.withZone(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if a Cookie marked as secure should be used. The default is to use the value
|
||||
* of {@link HttpServletRequest#isSecure()}.
|
||||
@@ -303,7 +308,7 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
/**
|
||||
* Sets the maxAge property of the Cookie. The default is to delete the cookie when
|
||||
* the browser is closed.
|
||||
* @param cookieMaxAge the maxAge property of the Cookie
|
||||
* @param cookieMaxAge the maxAge property of the Cookie (defined in seconds)
|
||||
*/
|
||||
public void setCookieMaxAge(int cookieMaxAge) {
|
||||
this.cookieMaxAge = cookieMaxAge;
|
||||
|
||||
@@ -65,11 +65,7 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
public void setSession(S session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public S getSession() {
|
||||
S getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
@@ -191,16 +187,16 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
|
||||
this.invalidated = true;
|
||||
}
|
||||
|
||||
public void setNew(boolean isNew) {
|
||||
this.old = !isNew;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNew() {
|
||||
checkState();
|
||||
return !this.old;
|
||||
}
|
||||
|
||||
void markNotNew() {
|
||||
this.old = true;
|
||||
}
|
||||
|
||||
private void checkState() {
|
||||
if (this.invalidated) {
|
||||
throw new IllegalStateException("The HttpSession has already be invalidated.");
|
||||
|
||||
@@ -25,9 +25,6 @@ import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Base class for response wrappers which encapsulate the logic for handling an event when
|
||||
* the {@link javax.servlet.http.HttpServletResponse} is committed.
|
||||
@@ -37,8 +34,6 @@ import org.apache.commons.logging.LogFactory;
|
||||
*/
|
||||
abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private boolean disableOnCommitted;
|
||||
|
||||
/**
|
||||
@@ -69,6 +64,12 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentLengthLong(long len) {
|
||||
setContentLength(len);
|
||||
super.setContentLengthLong(len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentLength(int len) {
|
||||
setContentLength((long) len);
|
||||
@@ -86,7 +87,7 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
|
||||
* {@link javax.servlet.http.HttpServletResponse} is committed. This can be useful in
|
||||
* the event that Async Web Requests are made.
|
||||
*/
|
||||
public void disableOnResponseCommitted() {
|
||||
private void disableOnResponseCommitted() {
|
||||
this.disableOnCommitted = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -292,7 +292,7 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
|
||||
requestedSession.setLastAccessedTime(Instant.now());
|
||||
this.requestedSessionIdValid = true;
|
||||
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
|
||||
currentSession.setNew(false);
|
||||
currentSession.markNotNew();
|
||||
setCurrentSession(currentSession);
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class DelegatingIndexResolverTests {
|
||||
this.supportedIndex = supportedIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> resolveIndexesFor(MapSession session) {
|
||||
return Collections.singletonMap(this.supportedIndex, session.getAttribute(this.supportedIndex));
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ class EnableSpringHttpSessionCustomCookieSerializerTests {
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public SessionRepository sessionRepository() {
|
||||
SessionRepository sessionRepository() {
|
||||
return mock(SessionRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CookieSerializer cookieSerializer() {
|
||||
CookieSerializer cookieSerializer() {
|
||||
return mock(CookieSerializer.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class SpringHttpSessionConfigurationTests {
|
||||
static class BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
public MapSessionRepository sessionRepository() {
|
||||
MapSessionRepository sessionRepository() {
|
||||
return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class SpringHttpSessionConfigurationTests {
|
||||
static class SessionCookieConfigConfiguration extends BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
public ServletContext servletContext() {
|
||||
ServletContext servletContext() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.getSessionCookieConfig().setName("test-name");
|
||||
servletContext.getSessionCookieConfig().setDomain("test-domain");
|
||||
@@ -153,7 +153,7 @@ class SpringHttpSessionConfigurationTests {
|
||||
static class RememberMeServicesConfiguration extends BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
public SpringSessionRememberMeServices rememberMeServices() {
|
||||
SpringSessionRememberMeServices rememberMeServices() {
|
||||
return new SpringSessionRememberMeServices();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.session.security;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
@@ -30,6 +31,7 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
@@ -104,11 +106,25 @@ class SpringSessionBackedSessionRegistryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllSessions() {
|
||||
void getAllSessionsForUserDetails() {
|
||||
setUpSessions();
|
||||
|
||||
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(PRINCIPAL, true);
|
||||
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllSessionsForAuthenticatedPrincipal() {
|
||||
setUpSessions();
|
||||
List<SessionInformation> allSessionInfos = this.sessionRegistry
|
||||
.getAllSessions((AuthenticatedPrincipal) () -> USER_NAME, true);
|
||||
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllSessionsForPrincipal() {
|
||||
setUpSessions();
|
||||
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(new TestPrincipal(USER_NAME),
|
||||
true);
|
||||
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
|
||||
}
|
||||
|
||||
@@ -159,4 +175,40 @@ class SpringSessionBackedSessionRegistryTest {
|
||||
when(this.sessionRepository.findByPrincipalName(USER_NAME)).thenReturn(sessions);
|
||||
}
|
||||
|
||||
private static final class TestPrincipal implements Principal {
|
||||
|
||||
private final String name;
|
||||
|
||||
private TestPrincipal(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object another) {
|
||||
if (this == another) {
|
||||
return true;
|
||||
}
|
||||
if (another instanceof TestPrincipal) {
|
||||
return this.name.equals(((TestPrincipal) another).name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.session.web.http;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
@@ -71,11 +76,10 @@ class DefaultCookieSerializerTests {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesSingle(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
|
||||
}
|
||||
|
||||
@@ -84,81 +88,73 @@ class DefaultCookieSerializerTests {
|
||||
this.sessionId = "&^%$*";
|
||||
this.serializer.setUseBase64Encoding(true);
|
||||
this.request.setCookies(new Cookie(this.cookieName, this.sessionId));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesSingleAndInvalidName(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
|
||||
createCookie(this.cookieName + "INVALID", this.sessionId + "INVALID", useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesMulti(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
String secondSession = "secondSessionId";
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
|
||||
createCookie(this.cookieName, secondSession, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsExactly(this.sessionId, secondSession);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesMultiCustomSessionCookieName(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
setCookieName("JSESSIONID");
|
||||
String secondSession = "secondSessionId";
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding),
|
||||
createCookie(this.cookieName, secondSession, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsExactly(this.sessionId, secondSession);
|
||||
}
|
||||
|
||||
// gh-392
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesNullCookieValue(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesNullCookieValueAndJvmRoute(boolean useBase64Encoding) {
|
||||
this.serializer.setJvmRoute("123");
|
||||
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieValuesNullCookieValueAndNotNullCookie(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
this.serializer.setJvmRoute("123");
|
||||
this.request.setCookies(createCookie(this.cookieName, null, useBase64Encoding),
|
||||
createCookie(this.cookieName, this.sessionId, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
|
||||
}
|
||||
|
||||
// --- writeCookie ---
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void writeCookie(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookieValue(useBase64Encoding)).isEqualTo(this.sessionId);
|
||||
}
|
||||
|
||||
@@ -167,25 +163,20 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieHttpOnlyDefault() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().isHttpOnly()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieHttpOnlySetTrue() {
|
||||
this.serializer.setUseHttpOnlyCookie(true);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().isHttpOnly()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieHttpOnlySetFalse() {
|
||||
this.serializer.setUseHttpOnlyCookie(false);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().isHttpOnly()).isFalse();
|
||||
}
|
||||
|
||||
@@ -194,7 +185,6 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieDomainNameDefault() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getDomain()).isNull();
|
||||
}
|
||||
|
||||
@@ -202,9 +192,7 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieDomainNameCustom() {
|
||||
String domainName = "example.com";
|
||||
this.serializer.setDomainName(domainName);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getDomain()).isEqualTo(domainName);
|
||||
}
|
||||
|
||||
@@ -221,22 +209,18 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieDomainNamePattern() {
|
||||
String domainNamePattern = "^.+?\\.(\\w+\\.[a-z]+)$";
|
||||
this.serializer.setDomainNamePattern(domainNamePattern);
|
||||
|
||||
String[] matchingDomains = { "child.sub.example.com", "www.example.com" };
|
||||
for (String domain : matchingDomains) {
|
||||
this.request.setServerName(domain);
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
assertThat(getCookie().getDomain()).isEqualTo("example.com");
|
||||
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
String[] notMatchingDomains = { "example.com", "localhost", "127.0.0.1" };
|
||||
for (String domain : notMatchingDomains) {
|
||||
this.request.setServerName(domain);
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
assertThat(getCookie().getDomain()).isNull();
|
||||
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
}
|
||||
@@ -253,7 +237,6 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieCookieNameDefault() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getName()).isEqualTo("SESSION");
|
||||
}
|
||||
|
||||
@@ -261,9 +244,7 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieCookieNameCustom() {
|
||||
String cookieName = "JSESSIONID";
|
||||
setCookieName(cookieName);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getName()).isEqualTo(cookieName);
|
||||
}
|
||||
|
||||
@@ -278,18 +259,14 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieCookiePathDefaultEmptyContextPathUsed() {
|
||||
this.request.setContextPath("");
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getPath()).isEqualTo("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieCookiePathDefaultContextPathUsed() {
|
||||
this.request.setContextPath("/context");
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getPath()).isEqualTo("/context/");
|
||||
}
|
||||
|
||||
@@ -297,9 +274,7 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieCookiePathExplicitNullCookiePathContextPathUsed() {
|
||||
this.request.setContextPath("/context");
|
||||
this.serializer.setCookiePath(null);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getPath()).isEqualTo("/context/");
|
||||
}
|
||||
|
||||
@@ -307,9 +282,7 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieCookiePathExplicitCookiePath() {
|
||||
this.request.setContextPath("/context");
|
||||
this.serializer.setCookiePath("/");
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getPath()).isEqualTo("/");
|
||||
}
|
||||
|
||||
@@ -318,36 +291,45 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieCookieMaxAgeDefault() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(-1);
|
||||
assertThat(getCookie().getExpires()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieCookieMaxAgeExplicit() {
|
||||
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
|
||||
this.serializer.setCookieMaxAge(100);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
MockCookie cookie = getCookie();
|
||||
assertThat(cookie.getMaxAge()).isEqualTo(100);
|
||||
ZonedDateTime expires = cookie.getExpires();
|
||||
assertThat(expires).isNotNull();
|
||||
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Mon, 7 Oct 2019 20:11:40 GMT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieCookieMaxAgeExplicitEmptyCookie() {
|
||||
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
|
||||
this.serializer.setCookieMaxAge(100);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(""));
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(0);
|
||||
MockCookie cookie = getCookie();
|
||||
assertThat(cookie.getMaxAge()).isEqualTo(0);
|
||||
ZonedDateTime expires = cookie.getExpires();
|
||||
assertThat(expires).isNotNull();
|
||||
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Thu, 1 Jan 1970 00:00:00 GMT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieCookieMaxAgeExplicitCookieValue() {
|
||||
this.serializer.setClock(Clock.fixed(Instant.parse("2019-10-07T20:10:00Z"), ZoneOffset.UTC));
|
||||
CookieValue cookieValue = cookieValue(this.sessionId);
|
||||
cookieValue.setCookieMaxAge(100);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue);
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
MockCookie cookie = getCookie();
|
||||
assertThat(cookie.getMaxAge()).isEqualTo(100);
|
||||
ZonedDateTime expires = cookie.getExpires();
|
||||
assertThat(expires).isNotNull();
|
||||
assertThat(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)).isEqualTo("Mon, 7 Oct 2019 20:11:40 GMT");
|
||||
}
|
||||
|
||||
// --- secure ---
|
||||
@@ -355,7 +337,6 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieDefaultInsecureRequest() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSecure()).isFalse();
|
||||
}
|
||||
|
||||
@@ -363,18 +344,14 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieSecureSecureRequest() {
|
||||
this.request.setSecure(true);
|
||||
this.serializer.setUseSecureCookie(true);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSecure()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieSecureInsecureRequest() {
|
||||
this.serializer.setUseSecureCookie(true);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSecure()).isTrue();
|
||||
}
|
||||
|
||||
@@ -382,65 +359,56 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieInsecureSecureRequest() {
|
||||
this.request.setSecure(true);
|
||||
this.serializer.setUseSecureCookie(false);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSecure()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeCookieInecureInsecureRequest() {
|
||||
this.serializer.setUseSecureCookie(false);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSecure()).isFalse();
|
||||
}
|
||||
|
||||
// --- jvmRoute ---
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void writeCookieJvmRoute(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
String jvmRoute = "route";
|
||||
this.serializer.setJvmRoute(jvmRoute);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookieValue(useBase64Encoding)).isEqualTo(this.sessionId + "." + jvmRoute);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieJvmRoute(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
String jvmRoute = "route";
|
||||
this.serializer.setJvmRoute(jvmRoute);
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId + "." + jvmRoute, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieJvmRouteRouteMissing(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
String jvmRoute = "route";
|
||||
this.serializer.setJvmRoute(jvmRoute);
|
||||
this.request.setCookies(createCookie(this.cookieName, this.sessionId, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly(this.sessionId);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "true", "false" })
|
||||
@ValueSource(booleans = { true, false })
|
||||
void readCookieJvmRouteOnlyRoute(boolean useBase64Encoding) {
|
||||
this.serializer.setUseBase64Encoding(useBase64Encoding);
|
||||
String jvmRoute = "route";
|
||||
this.serializer.setJvmRoute(jvmRoute);
|
||||
this.request.setCookies(createCookie(this.cookieName, "." + jvmRoute, useBase64Encoding));
|
||||
|
||||
assertThat(this.serializer.readCookieValues(this.request)).containsOnly("");
|
||||
}
|
||||
|
||||
@@ -451,7 +419,6 @@ class DefaultCookieSerializerTests {
|
||||
this.request.setAttribute("rememberMe", true);
|
||||
this.serializer.setRememberMeRequestAttribute("rememberMe");
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@@ -462,7 +429,6 @@ class DefaultCookieSerializerTests {
|
||||
CookieValue cookieValue = cookieValue(this.sessionId);
|
||||
cookieValue.setCookieMaxAge(100);
|
||||
this.serializer.writeCookieValue(cookieValue);
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@@ -471,7 +437,6 @@ class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
void writeCookieDefaultSameSiteLax() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
|
||||
}
|
||||
|
||||
@@ -479,7 +444,6 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieSetSameSiteLax() {
|
||||
this.serializer.setSameSite("Lax");
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
|
||||
}
|
||||
|
||||
@@ -487,7 +451,6 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieSetSameSiteStrict() {
|
||||
this.serializer.setSameSite("Strict");
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Strict");
|
||||
}
|
||||
|
||||
@@ -495,11 +458,10 @@ class DefaultCookieSerializerTests {
|
||||
void writeCookieSetSameSiteNull() {
|
||||
this.serializer.setSameSite(null);
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isNull();
|
||||
}
|
||||
|
||||
public void setCookieName(String cookieName) {
|
||||
void setCookieName(String cookieName) {
|
||||
this.cookieName = cookieName;
|
||||
this.serializer.setCookieName(cookieName);
|
||||
}
|
||||
|
||||
@@ -1100,6 +1100,17 @@ class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
// gh-7261
|
||||
@Test
|
||||
void contentLengthLongOutputStreamWriteStringCommits() throws IOException {
|
||||
String body = "something";
|
||||
this.response.setContentLengthLong(body.length());
|
||||
|
||||
this.response.getOutputStream().print(body);
|
||||
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bufferSizeCommitsOnce() throws Exception {
|
||||
String expected = "1234567890";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1331,11 +1331,8 @@ class SessionRepositoryFilterTests {
|
||||
|
||||
// We want the filter to work without any dependencies on Spring
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void doesNotImplementOrdered() {
|
||||
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> {
|
||||
Ordered o = (Ordered) this.filter;
|
||||
});
|
||||
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> Ordered.class.cast(this.filter));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,7 +5,6 @@ description = "Spring Session Redis implementation"
|
||||
dependencies {
|
||||
compile project(':spring-session-core')
|
||||
compile ("org.springframework.data:spring-data-redis") {
|
||||
exclude group: "org.slf4j", module: 'slf4j-api'
|
||||
exclude group: "org.slf4j", module: 'jcl-over-slf4j'
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
|
||||
/**
|
||||
* Base class for {@link RedisOperationsSessionRepository} integration tests.
|
||||
* Base class for Redis integration tests.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public abstract class AbstractRedisITests {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.5";
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.10";
|
||||
|
||||
protected static class BaseConfig {
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -34,21 +35,21 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveRedisOperationsSessionRepository}.
|
||||
* Integration tests for {@link ReactiveRedisSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
class ReactiveRedisSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
@Autowired
|
||||
private ReactiveRedisOperationsSessionRepository repository;
|
||||
private ReactiveRedisSessionRepository repository;
|
||||
|
||||
@Test
|
||||
void saves() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
|
||||
String expectedAttributeName = "a";
|
||||
String expectedAttributeValue = "b";
|
||||
@@ -70,7 +71,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
|
||||
@Test // gh-1399
|
||||
void saveMultipleTimes() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.createSession().block();
|
||||
RedisSession session = this.repository.createSession().block();
|
||||
session.setAttribute("attribute1", "value1");
|
||||
Mono<Void> save1 = this.repository.save(session);
|
||||
session.setAttribute("attribute2", "value2");
|
||||
@@ -80,7 +81,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
|
||||
@Test
|
||||
void putAllOnSingleAttrDoesNotRemoveOld() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute("a", "b");
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
@@ -103,13 +104,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
void changeSessionIdWhenOnlyChangeId() {
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
toSave.setAttribute(attrName, attrValue);
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
|
||||
.block();
|
||||
RedisSession findById = this.repository.findById(toSave.getId()).block();
|
||||
|
||||
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
|
||||
@@ -120,15 +120,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
|
||||
assertThat(this.repository.findById(originalFindById).block()).isNull();
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
|
||||
.findById(changeSessionId).block();
|
||||
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
|
||||
|
||||
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenChangeTwice() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
@@ -148,12 +147,11 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
|
||||
.block();
|
||||
RedisSession findById = this.repository.findById(toSave.getId()).block();
|
||||
|
||||
findById.setAttribute(attrName, attrValue);
|
||||
|
||||
@@ -164,15 +162,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
|
||||
assertThat(this.repository.findById(originalFindById).block()).isNull();
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
|
||||
.findById(changeSessionId).block();
|
||||
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
|
||||
|
||||
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenHasNotSaved() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String originalId = toSave.getId();
|
||||
toSave.changeSessionId();
|
||||
|
||||
@@ -185,7 +182,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
// gh-954
|
||||
@Test
|
||||
void changeSessionIdSaveTwice() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String originalId = toSave.getId();
|
||||
toSave.changeSessionId();
|
||||
|
||||
@@ -199,12 +196,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
|
||||
// gh-1111
|
||||
@Test
|
||||
void changeSessionSaveOldSessionInstance() {
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
|
||||
RedisSession toSave = this.repository.createSession().block();
|
||||
String sessionId = toSave.getId();
|
||||
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.findById(sessionId).block();
|
||||
RedisSession session = this.repository.findById(sessionId).block();
|
||||
session.changeSessionId();
|
||||
session.setLastAccessedTime(Instant.now());
|
||||
this.repository.save(session).block();
|
||||
@@ -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.
|
||||
@@ -37,7 +37,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.data.SessionEventRegistry;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
@@ -48,17 +48,23 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link RedisIndexedSessionRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
|
||||
|
||||
@Autowired
|
||||
private RedisOperationsSessionRepository repository;
|
||||
private RedisIndexedSessionRepository repository;
|
||||
|
||||
@Autowired
|
||||
private SessionEventRegistry registry;
|
||||
@@ -88,7 +94,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
void saves() throws InterruptedException {
|
||||
String username = "saves-" + System.currentTimeMillis();
|
||||
|
||||
String usernameSessionKey = "RedisOperationsSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
|
||||
String usernameSessionKey = "RedisIndexedSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
|
||||
|
||||
RedisSession toSave = this.repository.createSession();
|
||||
String expectedAttributeName = "a";
|
||||
@@ -180,7 +186,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
|
||||
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
|
||||
String channel = "__keyevent@0__:expired";
|
||||
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
@@ -342,7 +348,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
|
||||
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
|
||||
String channel = "__keyevent@0__:expired";
|
||||
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
@@ -607,11 +613,11 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession(redisNamespace = "RedisOperationsSessionRepositoryITests")
|
||||
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public SessionEventRegistry sessionEventRegistry() {
|
||||
SessionEventRegistry sessionEventRegistry() {
|
||||
return new SessionEventRegistry();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@@ -41,17 +41,17 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
|
||||
* Integration tests for {@link RedisSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
class RedisSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
@Autowired
|
||||
private SimpleRedisOperationsSessionRepository sessionRepository;
|
||||
private RedisSessionRepository sessionRepository;
|
||||
|
||||
@Test
|
||||
void save_NewSession_ShouldSaveSession() {
|
||||
@@ -227,11 +227,11 @@ class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public SimpleRedisOperationsSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return new SimpleRedisOperationsSessionRepository(redisTemplate);
|
||||
return new RedisSessionRepository(redisTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,11 +102,11 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
|
||||
}
|
||||
}
|
||||
|
||||
public boolean receivedEvent() {
|
||||
boolean receivedEvent() {
|
||||
return this.receivedEvent;
|
||||
}
|
||||
|
||||
public void setLock(Object lock) {
|
||||
void setLock(Object lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
|
||||
SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
|
||||
return new SessionExpiredEventRegistry();
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class RedisOperationsSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
|
||||
class RedisIndexedSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
|
||||
|
||||
@Autowired
|
||||
private SessionRepository<S> sessionRepository;
|
||||
@@ -88,7 +88,7 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean taskDispatched() throws InterruptedException {
|
||||
boolean taskDispatched() throws InterruptedException {
|
||||
if (this.taskDispatched != null) {
|
||||
return this.taskDispatched;
|
||||
}
|
||||
@@ -105,12 +105,12 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public Executor springSessionRedisTaskExecutor() {
|
||||
Executor springSessionRedisTaskExecutor() {
|
||||
return new SessionTaskExecutor(Executors.newSingleThreadExecutor());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Executor springSessionRedisSubscriptionExecutor() {
|
||||
Executor springSessionRedisSubscriptionExecutor() {
|
||||
return new SimpleAsyncTaskExecutor();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,71 +16,29 @@
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
|
||||
* {@link ReactiveRedisOperations}.
|
||||
* This {@link ReactiveSessionRepository} implementation is kept in order to support
|
||||
* migration to {@link ReactiveRedisSessionRepository} in a backwards compatible manner.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.0
|
||||
* @since 2.0.0
|
||||
* @deprecated since 2.2.0 in favor of {@link ReactiveRedisSessionRepository}
|
||||
*/
|
||||
public class ReactiveRedisOperationsSessionRepository
|
||||
implements ReactiveSessionRepository<ReactiveRedisOperationsSessionRepository.RedisSession> {
|
||||
@Deprecated
|
||||
public class ReactiveRedisOperationsSessionRepository extends ReactiveRedisSessionRepository {
|
||||
|
||||
/**
|
||||
* The default namespace for each key and channel in Redis used by Spring Session.
|
||||
* Create a new {@link ReactiveRedisOperationsSessionRepository} instance.
|
||||
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
|
||||
* managing sessions
|
||||
* @see ReactiveRedisSessionRepository#ReactiveRedisSessionRepository(ReactiveRedisOperations)
|
||||
*/
|
||||
public static final String DEFAULT_NAMESPACE = "spring:session";
|
||||
|
||||
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
|
||||
|
||||
/**
|
||||
* The namespace for every key used by Spring Session in Redis.
|
||||
*/
|
||||
private String namespace = DEFAULT_NAMESPACE + ":";
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
public ReactiveRedisOperationsSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
}
|
||||
|
||||
public void setRedisKeyNamespace(String namespace) {
|
||||
Assert.hasText(namespace, "namespace cannot be null or empty");
|
||||
this.namespace = namespace.trim() + ":";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets 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 number of seconds that the {@link Session}
|
||||
* should be kept alive between client requests.
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
super(sessionRedisOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,226 +51,4 @@ public class ReactiveRedisOperationsSessionRepository
|
||||
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ReactiveRedisOperations} used for sessions.
|
||||
* @return the {@link ReactiveRedisOperations} used for sessions
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
|
||||
return this.sessionRedisOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RedisSession> createSession() {
|
||||
return Mono.defer(() -> {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
RedisSession session = new RedisSession(cached, true);
|
||||
return Mono.just(session);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save(RedisSession session) {
|
||||
if (session.isNew) {
|
||||
return session.save();
|
||||
}
|
||||
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
|
||||
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
|
||||
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RedisSession> findById(String id) {
|
||||
String sessionKey = getSessionKey(id);
|
||||
|
||||
// @formatter:off
|
||||
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
|
||||
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
|
||||
.filter((map) -> !map.isEmpty())
|
||||
.map(new RedisSessionMapper(id))
|
||||
.filter((session) -> !session.isExpired())
|
||||
.map((session) -> new RedisSession(session, false))
|
||||
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(String id) {
|
||||
String sessionKey = getSessionKey(id);
|
||||
|
||||
return this.sessionRedisOperations.delete(sessionKey).then();
|
||||
}
|
||||
|
||||
private static String getAttributeKey(String attributeName) {
|
||||
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
|
||||
}
|
||||
|
||||
private String getSessionKey(String sessionId) {
|
||||
return this.namespace + "sessions:" + sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track of any attributes that have changed. When
|
||||
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
|
||||
* changed will be persisted.
|
||||
*/
|
||||
final class RedisSession implements Session {
|
||||
|
||||
private final MapSession cached;
|
||||
|
||||
private final Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private String originalSessionId;
|
||||
|
||||
RedisSession(MapSession cached, boolean isNew) {
|
||||
this.cached = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalSessionId = cached.getId();
|
||||
if (this.isNew) {
|
||||
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) cached.getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
if (this.isNew || (ReactiveRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
|
||||
cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
return this.cached.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.cached.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& ReactiveRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(getAttributeKey(attributeName), attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.cached.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.cached.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(getAttributeKey(attributeName), attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
this.cached.removeAttribute(attributeName);
|
||||
this.delta.put(getAttributeKey(attributeName), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.cached.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.cached.setLastAccessedTime(lastAccessedTime);
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.cached.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.cached.setMaxInactiveInterval(interval);
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.cached.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.cached.isExpired();
|
||||
}
|
||||
|
||||
private boolean hasChangedSessionId() {
|
||||
return !getId().equals(this.originalSessionId);
|
||||
}
|
||||
|
||||
private Mono<Void> save() {
|
||||
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
|
||||
}
|
||||
|
||||
private Mono<Void> saveDelta() {
|
||||
if (this.delta.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String sessionKey = getSessionKey(getId());
|
||||
Mono<Boolean> update = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash()
|
||||
.putAll(sessionKey, new HashMap<>(this.delta));
|
||||
Mono<Boolean> setTtl = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
|
||||
.expire(sessionKey, getMaxInactiveInterval());
|
||||
|
||||
return update.and(setTtl).and((s) -> {
|
||||
this.delta.clear();
|
||||
s.onComplete();
|
||||
}).then();
|
||||
}
|
||||
|
||||
private Mono<Void> saveChangeSessionId() {
|
||||
if (!hasChangedSessionId()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String sessionId = getId();
|
||||
|
||||
Publisher<Void> replaceSessionId = (s) -> {
|
||||
this.originalSessionId = sessionId;
|
||||
s.onComplete();
|
||||
};
|
||||
|
||||
if (this.isNew) {
|
||||
return Mono.from(replaceSessionId);
|
||||
}
|
||||
else {
|
||||
String originalSessionKey = getSessionKey(this.originalSessionId);
|
||||
String sessionKey = getSessionKey(sessionId);
|
||||
|
||||
return ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
|
||||
.rename(originalSessionKey, sessionKey).and(replaceSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
|
||||
* {@link ReactiveRedisOperations}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class ReactiveRedisSessionRepository
|
||||
implements ReactiveSessionRepository<ReactiveRedisSessionRepository.RedisSession> {
|
||||
|
||||
/**
|
||||
* The default namespace for each key and channel in Redis used by Spring Session.
|
||||
*/
|
||||
public static final String DEFAULT_NAMESPACE = "spring:session";
|
||||
|
||||
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
|
||||
|
||||
/**
|
||||
* The namespace for every key used by Spring Session in Redis.
|
||||
*/
|
||||
private String namespace = DEFAULT_NAMESPACE + ":";
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveRedisSessionRepository} instance.
|
||||
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
|
||||
* managing sessions
|
||||
*/
|
||||
public ReactiveRedisSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
}
|
||||
|
||||
public void setRedisKeyNamespace(String namespace) {
|
||||
Assert.hasText(namespace, "namespace cannot be null or empty");
|
||||
this.namespace = namespace.trim() + ":";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets 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 number of seconds that the {@link Session}
|
||||
* should be kept alive between client requests.
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ReactiveRedisOperations} used for sessions.
|
||||
* @return the {@link ReactiveRedisOperations} used for sessions
|
||||
*/
|
||||
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
|
||||
return this.sessionRedisOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RedisSession> createSession() {
|
||||
return Mono.defer(() -> {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
RedisSession session = new RedisSession(cached, true);
|
||||
return Mono.just(session);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save(RedisSession session) {
|
||||
if (session.isNew) {
|
||||
return session.save();
|
||||
}
|
||||
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
|
||||
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
|
||||
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RedisSession> findById(String id) {
|
||||
String sessionKey = getSessionKey(id);
|
||||
|
||||
// @formatter:off
|
||||
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
|
||||
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
|
||||
.filter((map) -> !map.isEmpty())
|
||||
.map(new RedisSessionMapper(id))
|
||||
.filter((session) -> !session.isExpired())
|
||||
.map((session) -> new RedisSession(session, false))
|
||||
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(String id) {
|
||||
String sessionKey = getSessionKey(id);
|
||||
|
||||
return this.sessionRedisOperations.delete(sessionKey).then();
|
||||
}
|
||||
|
||||
private static String getAttributeKey(String attributeName) {
|
||||
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
|
||||
}
|
||||
|
||||
private String getSessionKey(String sessionId) {
|
||||
return this.namespace + "sessions:" + sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track of any attributes that have changed. When
|
||||
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
|
||||
* changed will be persisted.
|
||||
*/
|
||||
final class RedisSession implements Session {
|
||||
|
||||
private final MapSession cached;
|
||||
|
||||
private final Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private String originalSessionId;
|
||||
|
||||
RedisSession(MapSession cached, boolean isNew) {
|
||||
this.cached = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalSessionId = cached.getId();
|
||||
if (this.isNew) {
|
||||
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) cached.getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
if (this.isNew || (ReactiveRedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
|
||||
cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
return this.cached.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.cached.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& ReactiveRedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(getAttributeKey(attributeName), attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.cached.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.cached.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(getAttributeKey(attributeName), attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
this.cached.removeAttribute(attributeName);
|
||||
this.delta.put(getAttributeKey(attributeName), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.cached.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.cached.setLastAccessedTime(lastAccessedTime);
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.cached.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.cached.setMaxInactiveInterval(interval);
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.cached.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.cached.isExpired();
|
||||
}
|
||||
|
||||
private boolean hasChangedSessionId() {
|
||||
return !getId().equals(this.originalSessionId);
|
||||
}
|
||||
|
||||
private Mono<Void> save() {
|
||||
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
|
||||
}
|
||||
|
||||
private Mono<Void> saveDelta() {
|
||||
if (this.delta.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String sessionKey = getSessionKey(getId());
|
||||
Mono<Boolean> update = ReactiveRedisSessionRepository.this.sessionRedisOperations.opsForHash()
|
||||
.putAll(sessionKey, new HashMap<>(this.delta));
|
||||
Mono<Boolean> setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
|
||||
getMaxInactiveInterval());
|
||||
|
||||
return update.and(setTtl).and((s) -> {
|
||||
this.delta.clear();
|
||||
s.onComplete();
|
||||
}).then();
|
||||
}
|
||||
|
||||
private Mono<Void> saveChangeSessionId() {
|
||||
if (!hasChangedSessionId()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String sessionId = getId();
|
||||
|
||||
Publisher<Void> replaceSessionId = (s) -> {
|
||||
this.originalSessionId = sessionId;
|
||||
s.onComplete();
|
||||
};
|
||||
|
||||
if (this.isNew) {
|
||||
return Mono.from(replaceSessionId);
|
||||
}
|
||||
else {
|
||||
String originalSessionKey = getSessionKey(this.originalSessionId);
|
||||
String sessionKey = getSessionKey(sessionId);
|
||||
|
||||
return ReactiveRedisSessionRepository.this.sessionRedisOperations.rename(originalSessionKey, sessionKey)
|
||||
.and(replaceSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
/*
|
||||
* 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.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.dao.NonTransientDataAccessException;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
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.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link org.springframework.session.SessionRepository} that is implemented using
|
||||
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
|
||||
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
|
||||
* . This implementation supports {@link SessionDeletedEvent} and
|
||||
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Creating a new instance</h2>
|
||||
*
|
||||
* A typical example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre>
|
||||
* RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
*
|
||||
* // ... configure redisTemplate ...
|
||||
*
|
||||
* RedisIndexedSessionRepository redisSessionRepository =
|
||||
* new RedisIndexedSessionRepository(redisTemplate);
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* For additional information on how to create a RedisTemplate, refer to the
|
||||
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
|
||||
* > Spring Data Redis Reference</a>.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Storage Details</h2>
|
||||
*
|
||||
* The sections below outline how Redis is updated for each operation. An example of
|
||||
* creating a new session can be found below. The subsequent sections describe the
|
||||
* details.
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Saving a Session</h3>
|
||||
*
|
||||
* <p>
|
||||
* Each session is stored in Redis as a
|
||||
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
|
||||
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
|
||||
* example of how each session is stored can be seen below.
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* In this example, the session following statements are true about the session:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
|
||||
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
|
||||
* GMT.</li>
|
||||
* <li>The session expires in 1800 seconds (30 minutes).</li>
|
||||
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
|
||||
* 1/1/1970 GMT.</li>
|
||||
* <li>The session has two attributes. The first is "attrName" with the value of
|
||||
* "someAttrValue". The second session attribute is named "attrName2" with the value of
|
||||
* "someAttrValue2".</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* <h3>Optimized Writes</h3>
|
||||
*
|
||||
* <p>
|
||||
* The {@link RedisIndexedSessionRepository.RedisSession} keeps track of the properties
|
||||
* that have changed and only updates those. This means if an attribute is written once
|
||||
* and read many times we only need to write that attribute once. For example, assume the
|
||||
* session attribute "sessionAttr2" from earlier was updated. The following would be
|
||||
* executed upon saving:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
|
||||
* </pre>
|
||||
*
|
||||
* <h3>SessionCreatedEvent</h3>
|
||||
*
|
||||
* <p>
|
||||
* When a session is created an event is sent to Redis with the channel of
|
||||
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
|
||||
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the session id. The body of the event will be
|
||||
* the session that was created.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If registered as a {@link MessageListener}, then {@link RedisIndexedSessionRepository}
|
||||
* will then translate the Redis message into a {@link SessionCreatedEvent}.
|
||||
* </p>
|
||||
*
|
||||
* <h3>Expiration</h3>
|
||||
*
|
||||
* <p>
|
||||
* An expiration is associated to each session using the
|
||||
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
|
||||
* {@link RedisIndexedSessionRepository.RedisSession#getMaxInactiveInterval()} . For
|
||||
* example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* You will note that the expiration that is set is 5 minutes after the session actually
|
||||
* expires. This is necessary so that the value of the session can be accessed when the
|
||||
* session expires. An expiration is set on the session itself five minutes after it
|
||||
* actually expires to ensure it is cleaned up, but only after we perform any necessary
|
||||
* processing.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
|
||||
* be returned. This means there is no need to check the expiration before using a session
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Spring Session relies on the expired and delete
|
||||
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
|
||||
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
|
||||
* associated with the Session are cleaned up. For example, when using Spring Session's
|
||||
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
|
||||
* connections associated with the session to be closed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Expiration is not tracked directly on the session key itself since this would mean the
|
||||
* session data would no longer be available. Instead a special session expires key is
|
||||
* used. In our example the expires key is:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* When a session expires key is deleted or expires, the keyspace notification triggers a
|
||||
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* One problem with relying on Redis expiration exclusively is that Redis makes no
|
||||
* guarantee of when the expired event will be fired if the key has not been accessed.
|
||||
* Specifically the background task that Redis uses to clean up expired keys is a low
|
||||
* priority task and may not trigger the key expiration. For additional details see
|
||||
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
|
||||
* the Redis documentation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
|
||||
* that each key is accessed when it is expected to expire. This means that if the TTL is
|
||||
* expired on the key, Redis will remove the key and fire the expired event when we try to
|
||||
* access the key.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For this reason, each session expiration is also tracked to the nearest minute. This
|
||||
* allows a background task to access the potentially expired sessions to ensure that
|
||||
* Redis expired events are fired in a more deterministic fashion. For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* The background task will then use these mappings to explicitly request each session
|
||||
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
|
||||
* deletes the key for us only if the TTL is expired.
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
|
||||
* a race condition that incorrectly identifies a key as expired when it is not. Short of
|
||||
* using distributed locks (which would kill our performance) there is no way to ensure
|
||||
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
|
||||
* the key is only removed if the TTL on that key is expired.
|
||||
* </p>
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class RedisIndexedSessionRepository
|
||||
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class);
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
/**
|
||||
* The default Redis database used by Spring Session.
|
||||
*/
|
||||
public static final int DEFAULT_DATABASE = 0;
|
||||
|
||||
/**
|
||||
* The default namespace for each key and channel in Redis used by Spring Session.
|
||||
*/
|
||||
public static final String DEFAULT_NAMESPACE = "spring:session";
|
||||
|
||||
private int database = DEFAULT_DATABASE;
|
||||
|
||||
/**
|
||||
* The namespace for every key used by Spring Session in Redis.
|
||||
*/
|
||||
private String namespace = DEFAULT_NAMESPACE + ":";
|
||||
|
||||
private String sessionCreatedChannelPrefix;
|
||||
|
||||
private String sessionDeletedChannel;
|
||||
|
||||
private String sessionExpiredChannel;
|
||||
|
||||
private final RedisOperations<Object, Object> sessionRedisOperations;
|
||||
|
||||
private final RedisSessionExpirationPolicy expirationPolicy;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = (event) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
|
||||
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Creates a new instance. For an example, refer to the class level javadoc.
|
||||
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
|
||||
* sessions. Cannot be null.
|
||||
*/
|
||||
public RedisIndexedSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
|
||||
this::getSessionKey);
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link SessionDestroyedEvent}. The default is to not publish a
|
||||
* {@link SessionDestroyedEvent}.
|
||||
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
|
||||
* to publish {@link SessionDestroyedEvent}. Cannot be null.
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets 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 number of seconds that the {@link Session}
|
||||
* should be kept alive between client requests.
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(int 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default redis serializer. Replaces default serializer which is based on
|
||||
* {@link JdkSerializationRedisSerializer}.
|
||||
* @param defaultSerializer the new default redis serializer
|
||||
*/
|
||||
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
|
||||
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
|
||||
this.defaultSerializer = defaultSerializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
|
||||
* @param database the database index to use
|
||||
*/
|
||||
public void setDatabase(int database) {
|
||||
this.database = database;
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
private void configureSessionChannels() {
|
||||
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
|
||||
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
|
||||
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RedisOperations} used for sessions.
|
||||
* @return the {@link RedisOperations} used for sessions
|
||||
*/
|
||||
public RedisOperations<Object, Object> getSessionRedisOperations() {
|
||||
return this.sessionRedisOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(RedisSession session) {
|
||||
session.save();
|
||||
if (session.isNew) {
|
||||
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
|
||||
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
|
||||
session.isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupExpiredSessions() {
|
||||
this.expirationPolicy.cleanExpiredSessions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession findById(String id) {
|
||||
return getSession(id, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
String principalKey = getPrincipalKey(indexValue);
|
||||
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
|
||||
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
|
||||
for (Object id : sessionIds) {
|
||||
RedisSession session = findById((String) id);
|
||||
if (session != null) {
|
||||
sessions.put(session.getId(), session);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the session.
|
||||
* @param id the session id
|
||||
* @param allowExpired if true, will also include expired sessions that have not been
|
||||
* deleted. If false, will ensure expired sessions are not returned.
|
||||
* @return the Redis session
|
||||
*/
|
||||
private RedisSession getSession(String id, boolean allowExpired) {
|
||||
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MapSession loaded = loadSession(id, entries);
|
||||
if (!allowExpired && loaded.isExpired()) {
|
||||
return null;
|
||||
}
|
||||
RedisSession result = new RedisSession(loaded, false);
|
||||
result.originalLastAccessTime = loaded.getLastAccessedTime();
|
||||
return result;
|
||||
}
|
||||
|
||||
private MapSession loadSession(String id, Map<Object, Object> entries) {
|
||||
MapSession loaded = new MapSession(id);
|
||||
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
|
||||
String key = (String) entry.getKey();
|
||||
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
|
||||
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
|
||||
}
|
||||
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
|
||||
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
|
||||
}
|
||||
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
|
||||
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
|
||||
}
|
||||
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
|
||||
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String sessionId) {
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupPrincipalIndex(session);
|
||||
this.expirationPolicy.onDelete(session);
|
||||
|
||||
String expireKey = getExpiredKey(session.getId());
|
||||
this.sessionRedisOperations.delete(expireKey);
|
||||
|
||||
session.setMaxInactiveInterval(Duration.ZERO);
|
||||
save(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession createSession() {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
RedisSession session = new RedisSession(cached, true);
|
||||
session.flushImmediateIfNecessary();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
byte[] messageChannel = message.getChannel();
|
||||
byte[] messageBody = message.getBody();
|
||||
|
||||
String channel = new String(messageChannel);
|
||||
|
||||
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
|
||||
// TODO: is this thread safe?
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
|
||||
handleCreated(loaded, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
String body = new String(messageBody);
|
||||
if (!body.startsWith(getExpiredKeyPrefix())) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
|
||||
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
|
||||
int beginIndex = body.lastIndexOf(":") + 1;
|
||||
int endIndex = body.length();
|
||||
String sessionId = body.substring(beginIndex, endIndex);
|
||||
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
|
||||
if (session == null) {
|
||||
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
|
||||
}
|
||||
|
||||
cleanupPrincipalIndex(session);
|
||||
|
||||
if (isDeleted) {
|
||||
handleDeleted(session);
|
||||
}
|
||||
else {
|
||||
handleExpired(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupPrincipalIndex(RedisSession session) {
|
||||
String sessionId = session.getId();
|
||||
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(session);
|
||||
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
if (principal != null) {
|
||||
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCreated(Map<Object, Object> loaded, String channel) {
|
||||
String id = channel.substring(channel.lastIndexOf(":") + 1);
|
||||
Session session = loadSession(id, loaded);
|
||||
publishEvent(new SessionCreatedEvent(this, session));
|
||||
}
|
||||
|
||||
private void handleDeleted(RedisSession session) {
|
||||
publishEvent(new SessionDeletedEvent(this, session));
|
||||
}
|
||||
|
||||
private void handleExpired(RedisSession session) {
|
||||
publishEvent(new SessionExpiredEvent(this, session));
|
||||
}
|
||||
|
||||
private void publishEvent(ApplicationEvent event) {
|
||||
try {
|
||||
this.eventPublisher.publishEvent(event);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.error("Error publishing " + event + ".", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRedisKeyNamespace(String namespace) {
|
||||
Assert.hasText(namespace, "namespace cannot be null or empty");
|
||||
this.namespace = namespace.trim() + ":";
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Hash key for this session by prefixing it appropriately.
|
||||
* @param sessionId the session id
|
||||
* @return the Hash key for this session by prefixing it appropriately.
|
||||
*/
|
||||
String getSessionKey(String sessionId) {
|
||||
return this.namespace + "sessions:" + sessionId;
|
||||
}
|
||||
|
||||
String getPrincipalKey(String principalName) {
|
||||
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
|
||||
+ principalName;
|
||||
}
|
||||
|
||||
String getExpirationsKey(long expiration) {
|
||||
return this.namespace + "expirations:" + expiration;
|
||||
}
|
||||
|
||||
private String getExpiredKey(String sessionId) {
|
||||
return getExpiredKeyPrefix() + sessionId;
|
||||
}
|
||||
|
||||
private String getSessionCreatedChannel(String sessionId) {
|
||||
return getSessionCreatedChannelPrefix() + sessionId;
|
||||
}
|
||||
|
||||
private String getExpiredKeyPrefix() {
|
||||
return this.namespace + "sessions:expires:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
|
||||
* The suffix is the session id of the session that was created.
|
||||
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
|
||||
* to
|
||||
*/
|
||||
public String getSessionCreatedChannelPrefix() {
|
||||
return this.sessionCreatedChannelPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
|
||||
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
|
||||
*/
|
||||
public String getSessionDeletedChannel() {
|
||||
return this.sessionDeletedChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
|
||||
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
|
||||
*/
|
||||
public String getSessionExpiredChannel() {
|
||||
return this.sessionExpiredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
|
||||
* @param sessionId the id of the {@link Session} to work with
|
||||
* @return the {@link BoundHashOperations} to operate on a {@link Session}
|
||||
*/
|
||||
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
|
||||
String key = getSessionKey(sessionId);
|
||||
return this.sessionRedisOperations.boundHashOps(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key for the specified session attribute.
|
||||
* @param attributeName the attribute name
|
||||
* @return the attribute key name
|
||||
*/
|
||||
static String getSessionAttrNameKey(String attributeName) {
|
||||
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track of any attributes that have changed. When
|
||||
* {@link RedisIndexedSessionRepository.RedisSession#saveDelta()} is invoked all the
|
||||
* attributes that have been changed will be persisted.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
final class RedisSession implements Session {
|
||||
|
||||
private final MapSession cached;
|
||||
|
||||
private Instant originalLastAccessTime;
|
||||
|
||||
private Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private String originalPrincipalName;
|
||||
|
||||
private String originalSessionId;
|
||||
|
||||
RedisSession(MapSession cached, boolean isNew) {
|
||||
this.cached = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalSessionId = cached.getId();
|
||||
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
|
||||
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
if (this.isNew) {
|
||||
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) cached.getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
|
||||
cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.cached.setLastAccessedTime(lastAccessedTime);
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.cached.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.cached.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
return this.cached.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.cached.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.cached.setMaxInactiveInterval(interval);
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.cached.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.cached.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& RedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.cached.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.cached.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
this.cached.removeAttribute(attributeName);
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), null);
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
private void flushImmediateIfNecessary() {
|
||||
if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
saveChangeSessionId();
|
||||
saveDelta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves any attributes that have been changed and updates the expiration of this
|
||||
* session.
|
||||
*/
|
||||
private void saveDelta() {
|
||||
if (this.delta.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String sessionId = getId();
|
||||
getSessionBoundHashOperations(sessionId).putAll(this.delta);
|
||||
String principalSessionKey = getSessionAttrNameKey(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
|
||||
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
|
||||
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
|
||||
if (this.originalPrincipalName != null) {
|
||||
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
|
||||
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
|
||||
.remove(sessionId);
|
||||
}
|
||||
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
|
||||
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
this.originalPrincipalName = principal;
|
||||
if (principal != null) {
|
||||
String principalRedisKey = getPrincipalKey(principal);
|
||||
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
|
||||
.add(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
this.delta = new HashMap<>(this.delta.size());
|
||||
|
||||
Long originalExpiration = (this.originalLastAccessTime != null)
|
||||
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
|
||||
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
|
||||
}
|
||||
|
||||
private void saveChangeSessionId() {
|
||||
String sessionId = getId();
|
||||
if (sessionId.equals(this.originalSessionId)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isNew) {
|
||||
String originalSessionIdKey = getSessionKey(this.originalSessionId);
|
||||
String sessionIdKey = getSessionKey(sessionId);
|
||||
try {
|
||||
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
|
||||
sessionIdKey);
|
||||
}
|
||||
catch (NonTransientDataAccessException ex) {
|
||||
handleErrNoSuchKeyError(ex);
|
||||
}
|
||||
String originalExpiredKey = getExpiredKey(this.originalSessionId);
|
||||
String expiredKey = getExpiredKey(sessionId);
|
||||
try {
|
||||
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
|
||||
}
|
||||
catch (NonTransientDataAccessException ex) {
|
||||
handleErrNoSuchKeyError(ex);
|
||||
}
|
||||
}
|
||||
this.originalSessionId = sessionId;
|
||||
}
|
||||
|
||||
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
|
||||
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,334 +16,31 @@
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.dao.NonTransientDataAccessException;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
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.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link org.springframework.session.SessionRepository} that is implemented using
|
||||
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
|
||||
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
|
||||
* . This implementation supports {@link SessionDeletedEvent} and
|
||||
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Creating a new instance</h2>
|
||||
*
|
||||
* A typical example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre>
|
||||
* RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
*
|
||||
* // ... configure redisTemplate ...
|
||||
*
|
||||
* RedisOperationsSessionRepository redisSessionRepository =
|
||||
* new RedisOperationsSessionRepository(redisTemplate);
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* For additional information on how to create a RedisTemplate, refer to the
|
||||
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
|
||||
* > Spring Data Redis Reference</a>.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Storage Details</h2>
|
||||
*
|
||||
* The sections below outline how Redis is updated for each operation. An example of
|
||||
* creating a new session can be found below. The subsequent sections describe the
|
||||
* details.
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Saving a Session</h3>
|
||||
*
|
||||
* <p>
|
||||
* Each session is stored in Redis as a
|
||||
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
|
||||
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
|
||||
* example of how each session is stored can be seen below.
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* In this example, the session following statements are true about the session:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
|
||||
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
|
||||
* GMT.</li>
|
||||
* <li>The session expires in 1800 seconds (30 minutes).</li>
|
||||
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
|
||||
* 1/1/1970 GMT.</li>
|
||||
* <li>The session has two attributes. The first is "attrName" with the value of
|
||||
* "someAttrValue". The second session attribute is named "attrName2" with the value of
|
||||
* "someAttrValue2".</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* <h3>Optimized Writes</h3>
|
||||
*
|
||||
* <p>
|
||||
* The
|
||||
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession}
|
||||
* keeps track of the properties that have changed and only updates those. This means if
|
||||
* an attribute is written once and read many times we only need to write that attribute
|
||||
* once. For example, assume the session attribute "sessionAttr2" from earlier was
|
||||
* updated. The following would be executed upon saving:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
|
||||
* </pre>
|
||||
*
|
||||
* <h3>SessionCreatedEvent</h3>
|
||||
*
|
||||
* <p>
|
||||
* When a session is created an event is sent to Redis with the channel of
|
||||
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
|
||||
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the event will be
|
||||
* the session that was created.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If registered as a {@link MessageListener}, then
|
||||
* {@link RedisOperationsSessionRepository} will then translate the Redis message into a
|
||||
* {@link SessionCreatedEvent}.
|
||||
* </p>
|
||||
*
|
||||
* <h3>Expiration</h3>
|
||||
*
|
||||
* <p>
|
||||
* An expiration is associated to each session using the
|
||||
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
|
||||
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}
|
||||
* . For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* You will note that the expiration that is set is 5 minutes after the session actually
|
||||
* expires. This is necessary so that the value of the session can be accessed when the
|
||||
* session expires. An expiration is set on the session itself five minutes after it
|
||||
* actually expires to ensure it is cleaned up, but only after we perform any necessary
|
||||
* processing.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
|
||||
* be returned. This means there is no need to check the expiration before using a session
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Spring Session relies on the expired and delete
|
||||
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
|
||||
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
|
||||
* associated with the Session are cleaned up. For example, when using Spring Session's
|
||||
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
|
||||
* connections associated with the session to be closed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Expiration is not tracked directly on the session key itself since this would mean the
|
||||
* session data would no longer be available. Instead a special session expires key is
|
||||
* used. In our example the expires key is:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* When a session expires key is deleted or expires, the keyspace notification triggers a
|
||||
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* One problem with relying on Redis expiration exclusively is that Redis makes no
|
||||
* guarantee of when the expired event will be fired if the key has not been accessed.
|
||||
* Specifically the background task that Redis uses to clean up expired keys is a low
|
||||
* priority task and may not trigger the key expiration. For additional details see
|
||||
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
|
||||
* the Redis documentation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
|
||||
* that each key is accessed when it is expected to expire. This means that if the TTL is
|
||||
* expired on the key, Redis will remove the key and fire the expired event when we try to
|
||||
* access the key.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For this reason, each session expiration is also tracked to the nearest minute. This
|
||||
* allows a background task to access the potentially expired sessions to ensure that
|
||||
* Redis expired events are fired in a more deterministic fashion. For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* The background task will then use these mappings to explicitly request each session
|
||||
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
|
||||
* deletes the key for us only if the TTL is expired.
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
|
||||
* a race condition that incorrectly identifies a key as expired when it is not. Short of
|
||||
* using distributed locks (which would kill our performance) there is no way to ensure
|
||||
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
|
||||
* the key is only removed if the TTL on that key is expired.
|
||||
* </p>
|
||||
* This {@link SessionRepository} implementation is kept in order to support migration to
|
||||
* {@link RedisIndexedSessionRepository} in a backwards compatible manner.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
* @since 1.0
|
||||
* @deprecated since 2.2.0 in favor of {@link RedisIndexedSessionRepository}
|
||||
*/
|
||||
public class RedisOperationsSessionRepository
|
||||
implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
/**
|
||||
* The default Redis database used by Spring Session.
|
||||
*/
|
||||
public static final int DEFAULT_DATABASE = 0;
|
||||
|
||||
/**
|
||||
* The default namespace for each key and channel in Redis used by Spring Session.
|
||||
*/
|
||||
public static final String DEFAULT_NAMESPACE = "spring:session";
|
||||
|
||||
private int database = RedisOperationsSessionRepository.DEFAULT_DATABASE;
|
||||
|
||||
/**
|
||||
* The namespace for every key used by Spring Session in Redis.
|
||||
*/
|
||||
private String namespace = DEFAULT_NAMESPACE + ":";
|
||||
|
||||
private String sessionCreatedChannelPrefix;
|
||||
|
||||
private String sessionDeletedChannel;
|
||||
|
||||
private String sessionExpiredChannel;
|
||||
|
||||
private final RedisOperations<Object, Object> sessionRedisOperations;
|
||||
|
||||
private final RedisSessionExpirationPolicy expirationPolicy;
|
||||
|
||||
private final IndexResolver<RedisSession> indexResolver;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = (event) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
@Deprecated
|
||||
public class RedisOperationsSessionRepository extends RedisIndexedSessionRepository {
|
||||
|
||||
/**
|
||||
* Creates a new instance. For an example, refer to the class level javadoc.
|
||||
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
|
||||
* sessions. Cannot be null.
|
||||
* @see RedisIndexedSessionRepository#RedisIndexedSessionRepository(RedisOperations)
|
||||
*/
|
||||
public RedisOperationsSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
|
||||
this::getSessionKey);
|
||||
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link SessionDestroyedEvent}. The default is to not publish a
|
||||
* {@link SessionDestroyedEvent}.
|
||||
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
|
||||
* to publish {@link SessionDestroyedEvent}. Cannot be null.
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets 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 number of seconds that the {@link Session}
|
||||
* should be kept alive between client requests.
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default redis serializer. Replaces default serializer which is based on
|
||||
* {@link JdkSerializationRedisSerializer}.
|
||||
* @param defaultSerializer the new default redis serializer
|
||||
*/
|
||||
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
|
||||
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
|
||||
this.defaultSerializer = defaultSerializer;
|
||||
super(sessionRedisOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,517 +54,4 @@ public class RedisOperationsSessionRepository
|
||||
setFlushMode(redisFlushMode.getFlushMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
|
||||
* @param database the database index to use
|
||||
*/
|
||||
public void setDatabase(int database) {
|
||||
this.database = database;
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
private void configureSessionChannels() {
|
||||
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
|
||||
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
|
||||
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RedisOperations} used for sessions.
|
||||
* @return the {@link RedisOperations} used for sessions
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public RedisOperations<Object, Object> getSessionRedisOperations() {
|
||||
return this.sessionRedisOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(RedisSession session) {
|
||||
session.save();
|
||||
if (session.isNew()) {
|
||||
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
|
||||
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
|
||||
session.setNew(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupExpiredSessions() {
|
||||
this.expirationPolicy.cleanExpiredSessions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession findById(String id) {
|
||||
return getSession(id, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
String principalKey = getPrincipalKey(indexValue);
|
||||
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
|
||||
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
|
||||
for (Object id : sessionIds) {
|
||||
RedisSession session = findById((String) id);
|
||||
if (session != null) {
|
||||
sessions.put(session.getId(), session);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the session.
|
||||
* @param id the session id
|
||||
* @param allowExpired if true, will also include expired sessions that have not been
|
||||
* deleted. If false, will ensure expired sessions are not returned.
|
||||
* @return the Redis session
|
||||
*/
|
||||
private RedisSession getSession(String id, boolean allowExpired) {
|
||||
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MapSession loaded = loadSession(id, entries);
|
||||
if (!allowExpired && loaded.isExpired()) {
|
||||
return null;
|
||||
}
|
||||
RedisSession result = new RedisSession(loaded, false);
|
||||
result.originalLastAccessTime = loaded.getLastAccessedTime();
|
||||
return result;
|
||||
}
|
||||
|
||||
private MapSession loadSession(String id, Map<Object, Object> entries) {
|
||||
MapSession loaded = new MapSession(id);
|
||||
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
|
||||
String key = (String) entry.getKey();
|
||||
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
|
||||
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
|
||||
}
|
||||
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
|
||||
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
|
||||
}
|
||||
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
|
||||
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
|
||||
}
|
||||
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
|
||||
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String sessionId) {
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupPrincipalIndex(session);
|
||||
this.expirationPolicy.onDelete(session);
|
||||
|
||||
String expireKey = getExpiredKey(session.getId());
|
||||
this.sessionRedisOperations.delete(expireKey);
|
||||
|
||||
session.setMaxInactiveInterval(Duration.ZERO);
|
||||
save(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession createSession() {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
RedisSession session = new RedisSession(cached, true);
|
||||
session.flushImmediateIfNecessary();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
byte[] messageChannel = message.getChannel();
|
||||
byte[] messageBody = message.getBody();
|
||||
|
||||
String channel = new String(messageChannel);
|
||||
|
||||
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
|
||||
// TODO: is this thread safe?
|
||||
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
|
||||
handleCreated(loaded, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
String body = new String(messageBody);
|
||||
if (!body.startsWith(getExpiredKeyPrefix())) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
|
||||
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
|
||||
int beginIndex = body.lastIndexOf(":") + 1;
|
||||
int endIndex = body.length();
|
||||
String sessionId = body.substring(beginIndex, endIndex);
|
||||
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
|
||||
if (session == null) {
|
||||
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
|
||||
}
|
||||
|
||||
cleanupPrincipalIndex(session);
|
||||
|
||||
if (isDeleted) {
|
||||
handleDeleted(session);
|
||||
}
|
||||
else {
|
||||
handleExpired(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupPrincipalIndex(RedisSession session) {
|
||||
String sessionId = session.getId();
|
||||
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(session);
|
||||
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
if (principal != null) {
|
||||
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCreated(Map<Object, Object> loaded, String channel) {
|
||||
String id = channel.substring(channel.lastIndexOf(":") + 1);
|
||||
Session session = loadSession(id, loaded);
|
||||
publishEvent(new SessionCreatedEvent(this, session));
|
||||
}
|
||||
|
||||
private void handleDeleted(RedisSession session) {
|
||||
publishEvent(new SessionDeletedEvent(this, session));
|
||||
}
|
||||
|
||||
private void handleExpired(RedisSession session) {
|
||||
publishEvent(new SessionExpiredEvent(this, session));
|
||||
}
|
||||
|
||||
private void publishEvent(ApplicationEvent event) {
|
||||
try {
|
||||
this.eventPublisher.publishEvent(event);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.error("Error publishing " + event + ".", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRedisKeyNamespace(String namespace) {
|
||||
Assert.hasText(namespace, "namespace cannot be null or empty");
|
||||
this.namespace = namespace.trim() + ":";
|
||||
configureSessionChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Hash key for this session by prefixing it appropriately.
|
||||
* @param sessionId the session id
|
||||
* @return the Hash key for this session by prefixing it appropriately.
|
||||
*/
|
||||
String getSessionKey(String sessionId) {
|
||||
return this.namespace + "sessions:" + sessionId;
|
||||
}
|
||||
|
||||
String getPrincipalKey(String principalName) {
|
||||
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
|
||||
+ principalName;
|
||||
}
|
||||
|
||||
String getExpirationsKey(long expiration) {
|
||||
return this.namespace + "expirations:" + expiration;
|
||||
}
|
||||
|
||||
private String getExpiredKey(String sessionId) {
|
||||
return getExpiredKeyPrefix() + sessionId;
|
||||
}
|
||||
|
||||
private String getSessionCreatedChannel(String sessionId) {
|
||||
return getSessionCreatedChannelPrefix() + sessionId;
|
||||
}
|
||||
|
||||
private String getExpiredKeyPrefix() {
|
||||
return this.namespace + "sessions:expires:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
|
||||
* The suffix is the session id of the session that was created.
|
||||
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
|
||||
* to
|
||||
*/
|
||||
public String getSessionCreatedChannelPrefix() {
|
||||
return this.sessionCreatedChannelPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
|
||||
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
|
||||
*/
|
||||
public String getSessionDeletedChannel() {
|
||||
return this.sessionDeletedChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
|
||||
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
|
||||
*/
|
||||
public String getSessionExpiredChannel() {
|
||||
return this.sessionExpiredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
|
||||
* @param sessionId the id of the {@link Session} to work with
|
||||
* @return the {@link BoundHashOperations} to operate on a {@link Session}
|
||||
*/
|
||||
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
|
||||
String key = getSessionKey(sessionId);
|
||||
return this.sessionRedisOperations.boundHashOps(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key for the specified session attribute.
|
||||
* @param attributeName the attribute name
|
||||
* @return the attribute key name
|
||||
*/
|
||||
static String getSessionAttrNameKey(String attributeName) {
|
||||
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track of any attributes that have changed. When
|
||||
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()}
|
||||
* is invoked all the attributes that have been changed will be persisted.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 1.0
|
||||
*/
|
||||
final class RedisSession implements Session {
|
||||
|
||||
private final MapSession cached;
|
||||
|
||||
private Instant originalLastAccessTime;
|
||||
|
||||
private Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private String originalPrincipalName;
|
||||
|
||||
private String originalSessionId;
|
||||
|
||||
RedisSession(MapSession cached, boolean isNew) {
|
||||
this.cached = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalSessionId = cached.getId();
|
||||
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
|
||||
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
if (this.isNew) {
|
||||
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) cached.getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
if (this.isNew || (RedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
|
||||
cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
public void setNew(boolean isNew) {
|
||||
this.isNew = isNew;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.cached.setLastAccessedTime(lastAccessedTime);
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.cached.isExpired();
|
||||
}
|
||||
|
||||
public boolean isNew() {
|
||||
return this.isNew;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.cached.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
return this.cached.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.cached.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.cached.setMaxInactiveInterval(interval);
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.cached.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.cached.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& RedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.cached.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.cached.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
this.cached.removeAttribute(attributeName);
|
||||
this.delta.put(getSessionAttrNameKey(attributeName), null);
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
private void flushImmediateIfNecessary() {
|
||||
if (RedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
saveChangeSessionId();
|
||||
saveDelta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves any attributes that have been changed and updates the expiration of this
|
||||
* session.
|
||||
*/
|
||||
private void saveDelta() {
|
||||
if (this.delta.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String sessionId = getId();
|
||||
getSessionBoundHashOperations(sessionId).putAll(this.delta);
|
||||
String principalSessionKey = getSessionAttrNameKey(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
|
||||
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
|
||||
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
|
||||
if (this.originalPrincipalName != null) {
|
||||
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
|
||||
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
|
||||
.remove(sessionId);
|
||||
}
|
||||
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(this);
|
||||
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
|
||||
this.originalPrincipalName = principal;
|
||||
if (principal != null) {
|
||||
String principalRedisKey = getPrincipalKey(principal);
|
||||
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
|
||||
.add(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
this.delta = new HashMap<>(this.delta.size());
|
||||
|
||||
Long originalExpiration = (this.originalLastAccessTime != null)
|
||||
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
|
||||
RedisOperationsSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
|
||||
}
|
||||
|
||||
private void saveChangeSessionId() {
|
||||
String sessionId = getId();
|
||||
if (sessionId.equals(this.originalSessionId)) {
|
||||
return;
|
||||
}
|
||||
if (!isNew()) {
|
||||
String originalSessionIdKey = getSessionKey(this.originalSessionId);
|
||||
String sessionIdKey = getSessionKey(sessionId);
|
||||
try {
|
||||
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
|
||||
sessionIdKey);
|
||||
}
|
||||
catch (NonTransientDataAccessException ex) {
|
||||
handleErrNoSuchKeyError(ex);
|
||||
}
|
||||
String originalExpiredKey = getExpiredKey(this.originalSessionId);
|
||||
String expiredKey = getExpiredKey(sessionId);
|
||||
try {
|
||||
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
|
||||
}
|
||||
catch (NonTransientDataAccessException ex) {
|
||||
handleErrNoSuchKeyError(ex);
|
||||
}
|
||||
}
|
||||
this.originalSessionId = sessionId;
|
||||
}
|
||||
|
||||
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
|
||||
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
|
||||
|
||||
/**
|
||||
* A strategy for expiring {@link RedisSession} instances. This performs two operations:
|
||||
@@ -64,13 +64,13 @@ final class RedisSessionExpirationPolicy {
|
||||
this.lookupSessionKey = lookupSessionKey;
|
||||
}
|
||||
|
||||
public void onDelete(Session session) {
|
||||
void onDelete(Session session) {
|
||||
long toExpire = roundUpToNextMinute(expiresInMillis(session));
|
||||
String expireKey = getExpirationKey(toExpire);
|
||||
this.redis.boundSetOps(expireKey).remove(session.getId());
|
||||
}
|
||||
|
||||
public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
|
||||
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
|
||||
String keyToExpire = "expires:" + session.getId();
|
||||
long toExpire = roundUpToNextMinute(expiresInMillis(session));
|
||||
|
||||
@@ -117,7 +117,7 @@ final class RedisSessionExpirationPolicy {
|
||||
return this.lookupSessionKey.apply(sessionId);
|
||||
}
|
||||
|
||||
public void cleanExpiredSessions() {
|
||||
void cleanExpiredSessions() {
|
||||
long now = System.currentTimeMillis();
|
||||
long prevMin = roundDownMinute(now);
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ import org.springframework.util.Assert;
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class SimpleRedisOperationsSessionRepository
|
||||
implements SessionRepository<SimpleRedisOperationsSessionRepository.RedisSession> {
|
||||
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
|
||||
|
||||
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
|
||||
|
||||
@@ -56,11 +55,11 @@ public class SimpleRedisOperationsSessionRepository
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Create a new {@link SimpleRedisOperationsSessionRepository} instance.
|
||||
* Create a new {@link RedisSessionRepository} instance.
|
||||
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
|
||||
* sessions
|
||||
*/
|
||||
public SimpleRedisOperationsSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
|
||||
public RedisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations mut not be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
}
|
||||
@@ -182,7 +181,7 @@ public class SimpleRedisOperationsSessionRepository
|
||||
(int) cached.getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
if (this.isNew || (SimpleRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
if (this.isNew || (RedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
|
||||
cached.getAttribute(attributeName)));
|
||||
}
|
||||
@@ -201,8 +200,7 @@ public class SimpleRedisOperationsSessionRepository
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.cached.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& SimpleRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
if (attributeValue != null && RedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(getAttributeKey(attributeName), attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
@@ -260,7 +258,7 @@ public class SimpleRedisOperationsSessionRepository
|
||||
}
|
||||
|
||||
private void flushIfRequired() {
|
||||
if (SimpleRedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
if (RedisSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -282,8 +280,7 @@ public class SimpleRedisOperationsSessionRepository
|
||||
if (!this.isNew) {
|
||||
String originalSessionIdKey = getSessionKey(this.originalSessionId);
|
||||
String sessionIdKey = getSessionKey(getId());
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
|
||||
sessionIdKey);
|
||||
RedisSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);
|
||||
}
|
||||
this.originalSessionId = getId();
|
||||
}
|
||||
@@ -294,9 +291,8 @@ public class SimpleRedisOperationsSessionRepository
|
||||
return;
|
||||
}
|
||||
String key = getSessionKey(getId());
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key,
|
||||
new HashMap<>(this.delta));
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.expireAt(key,
|
||||
RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap<>(this.delta));
|
||||
RedisSessionRepository.this.sessionRedisOperations.expireAt(key,
|
||||
Date.from(Instant.ofEpochMilli(getLastAccessedTime().toEpochMilli())
|
||||
.plusSeconds(getMaxInactiveInterval().getSeconds())));
|
||||
this.delta.clear();
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -81,7 +81,7 @@ public class ConfigureNotifyKeyspaceEventsAction implements ConfigureRedisAction
|
||||
}
|
||||
catch (InvalidDataAccessApiUsageException ex) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent",
|
||||
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisindexedsessionrepository-sessiondestroyedevent",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
|
||||
/**
|
||||
* Qualifier annotation for a {@link RedisConnectionFactory} to be injected in
|
||||
* {@link RedisOperationsSessionRepository}.
|
||||
* {@link RedisIndexedSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.0.0
|
||||
|
||||
@@ -23,14 +23,18 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisSessionRepository;
|
||||
|
||||
/**
|
||||
* Annotation used to inject the Redis accessor used by Spring Session's Redis session
|
||||
* repository.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @see org.springframework.session.data.redis.RedisOperationsSessionRepository#getSessionRedisOperations()
|
||||
* @see org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository#getSessionRedisOperations()
|
||||
* @see RedisIndexedSessionRepository#getSessionRedisOperations()
|
||||
* @see RedisSessionRepository#getSessionRedisOperations()
|
||||
* @see ReactiveRedisSessionRepository#getSessionRedisOperations()
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
|
||||
/**
|
||||
@@ -65,7 +65,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(RedisHttpSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableRedisHttpSession {
|
||||
|
||||
/**
|
||||
@@ -85,7 +85,7 @@ public @interface EnableRedisHttpSession {
|
||||
* the applications and they could function within the same Redis instance.
|
||||
* @return the unique namespace for keys
|
||||
*/
|
||||
String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
|
||||
String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
|
||||
|
||||
/**
|
||||
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
|
||||
|
||||
@@ -18,8 +18,10 @@ package org.springframework.session.data.redis.config.annotation.web.http;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
@@ -49,11 +51,14 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.IndexResolver;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.config.SessionRepositoryCustomizer;
|
||||
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
|
||||
import org.springframework.session.data.redis.config.ConfigureRedisAction;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
|
||||
@@ -75,15 +80,14 @@ import org.springframework.util.StringValueResolver;
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableScheduling
|
||||
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer {
|
||||
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
|
||||
|
||||
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
|
||||
|
||||
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
|
||||
|
||||
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
|
||||
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
@@ -95,6 +99,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
private RedisConnectionFactory redisConnectionFactory;
|
||||
|
||||
private IndexResolver<Session> indexResolver;
|
||||
|
||||
private RedisSerializer<Object> defaultRedisSerializer;
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@@ -103,15 +109,20 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
private Executor redisSubscriptionExecutor;
|
||||
|
||||
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
|
||||
|
||||
private ClassLoader classLoader;
|
||||
|
||||
private StringValueResolver embeddedValueResolver;
|
||||
|
||||
@Bean
|
||||
public RedisOperationsSessionRepository sessionRepository() {
|
||||
public RedisIndexedSessionRepository sessionRepository() {
|
||||
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
|
||||
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
|
||||
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
|
||||
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
|
||||
if (this.indexResolver != null) {
|
||||
sessionRepository.setIndexResolver(this.indexResolver);
|
||||
}
|
||||
if (this.defaultRedisSerializer != null) {
|
||||
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
|
||||
}
|
||||
@@ -123,12 +134,14 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
sessionRepository.setSaveMode(this.saveMode);
|
||||
int database = resolveDatabase();
|
||||
sessionRepository.setDatabase(database);
|
||||
this.sessionRepositoryCustomizers
|
||||
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
|
||||
return sessionRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
|
||||
RedisOperationsSessionRepository sessionRepository) {
|
||||
RedisIndexedSessionRepository sessionRepository) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(this.redisConnectionFactory);
|
||||
if (this.redisTaskExecutor != null) {
|
||||
@@ -209,6 +222,11 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setIndexResolver(IndexResolver<Session> indexResolver) {
|
||||
this.indexResolver = indexResolver;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@Qualifier("springSessionRedisTaskExecutor")
|
||||
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
|
||||
@@ -221,6 +239,12 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
@@ -255,11 +279,6 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(), this.cleanupCron);
|
||||
}
|
||||
|
||||
private RedisTemplate<Object, Object> createRedisTemplate() {
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
@@ -282,7 +301,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
|
||||
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
|
||||
}
|
||||
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
|
||||
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,4 +344,25 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration of scheduled job for cleaning up expired sessions.
|
||||
*/
|
||||
@EnableScheduling
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class SessionCleanupConfiguration implements SchedulingConfigurer {
|
||||
|
||||
private final RedisIndexedSessionRepository sessionRepository;
|
||||
|
||||
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
|
||||
RedisHttpSessionConfiguration.this.cleanupCron);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
@@ -63,7 +63,7 @@ import org.springframework.web.server.session.WebSessionManager;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(RedisWebSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableRedisWebSession {
|
||||
|
||||
/**
|
||||
@@ -83,7 +83,7 @@ public @interface EnableRedisWebSession {
|
||||
* the applications and they could function within the same Redis instance.
|
||||
* @return the unique namespace for keys
|
||||
*/
|
||||
String redisNamespace() default ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
|
||||
String redisNamespace() default ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
|
||||
|
||||
/**
|
||||
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.session.data.redis.config.annotation.web.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -36,8 +38,9 @@ import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
|
||||
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -60,7 +63,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
|
||||
|
||||
private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
|
||||
private String redisNamespace = ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
@@ -68,20 +71,23 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
private RedisSerializer<Object> defaultRedisSerializer;
|
||||
|
||||
private List<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers;
|
||||
|
||||
private ClassLoader classLoader;
|
||||
|
||||
private StringValueResolver embeddedValueResolver;
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisOperationsSessionRepository sessionRepository() {
|
||||
public ReactiveRedisSessionRepository sessionRepository() {
|
||||
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate = createReactiveRedisTemplate();
|
||||
ReactiveRedisOperationsSessionRepository sessionRepository = new ReactiveRedisOperationsSessionRepository(
|
||||
reactiveRedisTemplate);
|
||||
ReactiveRedisSessionRepository sessionRepository = new ReactiveRedisSessionRepository(reactiveRedisTemplate);
|
||||
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
|
||||
if (StringUtils.hasText(this.redisNamespace)) {
|
||||
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
|
||||
}
|
||||
sessionRepository.setSaveMode(this.saveMode);
|
||||
this.sessionRepositoryCustomizers
|
||||
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
|
||||
return sessionRepository;
|
||||
}
|
||||
|
||||
@@ -120,6 +126,12 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
this.defaultRedisSerializer = defaultRedisSerializer;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.data.redis.core.ReactiveHashOperations;
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -46,11 +46,11 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveRedisOperationsSessionRepository}.
|
||||
* Tests for {@link ReactiveRedisSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
class ReactiveRedisSessionRepositoryTests {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ReactiveRedisOperations<String, Object> redisOperations = mock(ReactiveRedisOperations.class);
|
||||
@@ -61,13 +61,13 @@ class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
private ArgumentCaptor<Map<String, Object>> delta = ArgumentCaptor.forClass(Map.class);
|
||||
|
||||
private ReactiveRedisOperationsSessionRepository repository;
|
||||
private ReactiveRedisSessionRepository repository;
|
||||
|
||||
private MapSession cached;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.repository = new ReactiveRedisOperationsSessionRepository(this.redisOperations);
|
||||
this.repository = new ReactiveRedisSessionRepository(this.redisOperations);
|
||||
|
||||
this.cached = new MapSession();
|
||||
this.cached.setId("session-id");
|
||||
@@ -77,7 +77,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
void constructorWithNullReactiveRedisOperations() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
|
||||
.withMessageContaining("sessionRedisOperations cannot be null");
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
verifyZeroInteractions(this.hashOperations);
|
||||
|
||||
assertThat(this.delta.getAllValues().get(0)).isEqualTo(
|
||||
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -229,7 +229,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
verifyZeroInteractions(this.hashOperations);
|
||||
|
||||
assertThat(this.delta.getAllValues().get(0))
|
||||
.isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
|
||||
.isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -37,8 +37,6 @@ import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.connection.DefaultMessage;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.BoundValueOperations;
|
||||
@@ -50,7 +48,7 @@ import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -67,49 +65,40 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
|
||||
class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
|
||||
class RedisIndexedSessionRepositoryTests {
|
||||
|
||||
@Mock
|
||||
RedisConnectionFactory factory;
|
||||
private RedisOperations<Object, Object> redisOperations;
|
||||
|
||||
@Mock
|
||||
RedisConnection connection;
|
||||
private BoundValueOperations<Object, Object> boundValueOperations;
|
||||
|
||||
@Mock
|
||||
RedisOperations<Object, Object> redisOperations;
|
||||
private BoundHashOperations<Object, Object, Object> boundHashOperations;
|
||||
|
||||
@Mock
|
||||
BoundValueOperations<Object, Object> boundValueOperations;
|
||||
private BoundSetOperations<Object, Object> boundSetOperations;
|
||||
|
||||
@Mock
|
||||
BoundHashOperations<Object, Object, Object> boundHashOperations;
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
@Mock
|
||||
BoundSetOperations<Object, Object> boundSetOperations;
|
||||
|
||||
@Mock
|
||||
ApplicationEventPublisher publisher;
|
||||
|
||||
@Mock
|
||||
RedisSerializer<Object> defaultSerializer;
|
||||
private RedisSerializer<Object> defaultSerializer;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<AbstractSessionEvent> event;
|
||||
private ArgumentCaptor<AbstractSessionEvent> event;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Map<String, Object>> delta;
|
||||
private ArgumentCaptor<Map<String, Object>> delta;
|
||||
|
||||
private MapSession cached;
|
||||
|
||||
private RedisOperationsSessionRepository redisRepository;
|
||||
private RedisIndexedSessionRepository redisRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.redisRepository = new RedisOperationsSessionRepository(this.redisOperations);
|
||||
this.redisRepository = new RedisIndexedSessionRepository(this.redisOperations);
|
||||
this.redisRepository.setDefaultSerializer(this.defaultSerializer);
|
||||
|
||||
this.cached = new MapSession();
|
||||
@@ -277,7 +266,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
this.redisRepository.save(session);
|
||||
|
||||
assertThat(getDelta()).isEqualTo(
|
||||
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -291,7 +280,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
this.redisRepository.save(session);
|
||||
|
||||
assertThat(getDelta()).isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
|
||||
assertThat(getDelta()).isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -320,6 +309,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void delete() {
|
||||
String attrName = "attrName";
|
||||
MapSession expected = new MapSession();
|
||||
@@ -327,7 +317,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
expected.setAttribute(attrName, "attrValue");
|
||||
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
|
||||
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
|
||||
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
|
||||
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
|
||||
RedisSessionMapper.CREATION_TIME_KEY, expected.getCreationTime().toEpochMilli(),
|
||||
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) expected.getMaxInactiveInterval().getSeconds(),
|
||||
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, expected.getLastAccessedTime().toEpochMilli());
|
||||
@@ -353,6 +343,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void getSessionNotFound() {
|
||||
String id = "abc";
|
||||
given(this.redisOperations.boundHashOps(getKey(id))).willReturn(this.boundHashOperations);
|
||||
@@ -362,6 +353,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void getSessionFound() {
|
||||
String attribute1 = "attribute1";
|
||||
String attribute2 = "attribute2";
|
||||
@@ -370,8 +362,8 @@ class RedisOperationsSessionRepositoryTests {
|
||||
expected.setAttribute(attribute1, "test");
|
||||
expected.setAttribute(attribute2, null);
|
||||
given(this.redisOperations.boundHashOps(getKey(expected.getId()))).willReturn(this.boundHashOperations);
|
||||
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attribute1),
|
||||
expected.getAttribute(attribute1), RedisOperationsSessionRepository.getSessionAttrNameKey(attribute2),
|
||||
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attribute1),
|
||||
expected.getAttribute(attribute1), RedisIndexedSessionRepository.getSessionAttrNameKey(attribute2),
|
||||
expected.getAttribute(attribute2), RedisSessionMapper.CREATION_TIME_KEY,
|
||||
expected.getCreationTime().toEpochMilli(), RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) expected.getMaxInactiveInterval().getSeconds(), RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
|
||||
@@ -391,6 +383,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void getSessionExpired() {
|
||||
String expiredId = "expired-id";
|
||||
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
|
||||
@@ -402,6 +395,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void findByPrincipalNameExpired() {
|
||||
String expiredId = "expired-id";
|
||||
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
|
||||
@@ -417,6 +411,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void findByPrincipalName() {
|
||||
Instant lastAccessed = Instant.now().minusMillis(10);
|
||||
Instant createdTime = lastAccessed.minusMillis(10);
|
||||
@@ -446,7 +441,6 @@ class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
void cleanupExpiredSessions() {
|
||||
String expiredId = "expired-id";
|
||||
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
|
||||
|
||||
Set<Object> expiredIds = new HashSet<>(Arrays.asList("expired-key1", "expired-key2"));
|
||||
@@ -497,6 +491,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void onMessageDeletedSessionFound() {
|
||||
String deletedId = "deleted-id";
|
||||
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
|
||||
@@ -523,6 +518,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void onMessageDeletedSessionNotFound() {
|
||||
String deletedId = "deleted-id";
|
||||
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
|
||||
@@ -545,6 +541,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void onMessageExpiredSessionFound() {
|
||||
String expiredId = "expired-id";
|
||||
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
|
||||
@@ -571,6 +568,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void onMessageExpiredSessionNotFound() {
|
||||
String expiredId = "expired-id";
|
||||
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
|
||||
@@ -677,7 +675,7 @@ class RedisOperationsSessionRepositoryTests {
|
||||
Map<String, Object> delta = getDelta(2);
|
||||
assertThat(delta.size()).isEqualTo(1);
|
||||
assertThat(delta).isEqualTo(
|
||||
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -694,10 +692,11 @@ class RedisOperationsSessionRepositoryTests {
|
||||
Map<String, Object> delta = getDelta(2);
|
||||
assertThat(delta.size()).isEqualTo(1);
|
||||
assertThat(delta).isEqualTo(
|
||||
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void flushModeSetMaxInactiveIntervalInSeconds() {
|
||||
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
|
||||
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
|
||||
@@ -736,12 +735,6 @@ class RedisOperationsSessionRepositoryTests {
|
||||
.withMessage("flushMode cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setRedisFlushModeNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setRedisFlushMode(null))
|
||||
.withMessage("redisFlushMode cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRedisNamespace() {
|
||||
String namespace = "foo:bar";
|
||||
@@ -66,7 +66,7 @@ class RedisSessionExpirationPolicyTests {
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
RedisOperationsSessionRepository repository = new RedisOperationsSessionRepository(this.sessionRedisOperations);
|
||||
RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(this.sessionRedisOperations);
|
||||
this.policy = new RedisSessionExpirationPolicy(this.sessionRedisOperations, repository::getExpirationsKey,
|
||||
repository::getSessionKey);
|
||||
this.session = new MapSession();
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -48,11 +48,11 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleRedisOperationsSessionRepository}.
|
||||
* Tests for {@link RedisSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
class SimpleRedisOperationsSessionRepositoryTests {
|
||||
class RedisSessionRepositoryTests {
|
||||
|
||||
private static final String TEST_SESSION_ID = "session-id";
|
||||
|
||||
@@ -67,18 +67,18 @@ class SimpleRedisOperationsSessionRepositoryTests {
|
||||
@Captor
|
||||
private ArgumentCaptor<Map<String, Object>> delta;
|
||||
|
||||
private SimpleRedisOperationsSessionRepository sessionRepository;
|
||||
private RedisSessionRepository sessionRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
given(this.sessionRedisOperations.<String, Object>opsForHash()).willReturn(this.sessionHashOperations);
|
||||
this.sessionRepository = new SimpleRedisOperationsSessionRepository(this.sessionRedisOperations);
|
||||
this.sessionRepository = new RedisSessionRepository(this.sessionRedisOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_NullRedisOperations_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
|
||||
.withMessageContaining("sessionRedisOperations cannot be null");
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public static ConfigureRedisAction configureRedisAction() {
|
||||
ConfigureRedisAction configureRedisAction() {
|
||||
return ConfigureRedisAction.NO_OP;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory redisConnectionFactory() {
|
||||
return mock(RedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideDefaultSerializerTests {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
return mock(RedisSerializer.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
|
||||
RedisConnection connection = mock(RedisConnection.class);
|
||||
given(factory.getConnection()).willReturn(connection);
|
||||
|
||||
@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Executor springSessionRedisTaskExecutor() {
|
||||
Executor springSessionRedisTaskExecutor() {
|
||||
return mock(Executor.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
|
||||
RedisConnection connection = mock(RedisConnection.class);
|
||||
given(factory.getConnection()).willReturn(connection);
|
||||
|
||||
@@ -71,17 +71,17 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Executor springSessionRedisTaskExecutor() {
|
||||
Executor springSessionRedisTaskExecutor() {
|
||||
return mock(Executor.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Executor springSessionRedisSubscriptionExecutor() {
|
||||
Executor springSessionRedisSubscriptionExecutor() {
|
||||
return mock(Executor.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory connectionFactory() {
|
||||
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
|
||||
RedisConnection connection = mock(RedisConnection.class);
|
||||
given(factory.getConnection()).willReturn(connection);
|
||||
|
||||
@@ -29,15 +29,19 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.IndexResolver;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.config.SessionRepositoryCustomizer;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@@ -54,9 +58,10 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Mark Paluch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class RedisHttpSessionConfigurationTests {
|
||||
|
||||
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
|
||||
|
||||
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
|
||||
|
||||
private AnnotationConfigApplicationContext context;
|
||||
@@ -93,8 +98,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void customFlushImmediately() {
|
||||
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
|
||||
RedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -102,8 +106,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void customFlushImmediatelyLegacy() {
|
||||
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
|
||||
RedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -111,8 +114,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void setCustomFlushImmediately() {
|
||||
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
|
||||
RedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -120,8 +122,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void setCustomFlushImmediatelyLegacy() {
|
||||
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
|
||||
RedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -147,14 +148,14 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void customSaveModeAnnotation() {
|
||||
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
|
||||
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSaveModeSetter() {
|
||||
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
|
||||
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@@ -162,7 +163,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
void qualifiedConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
|
||||
|
||||
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
|
||||
RedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -178,7 +179,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
void primaryConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
|
||||
|
||||
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
|
||||
RedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -194,7 +195,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
|
||||
|
||||
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
|
||||
RedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -210,7 +211,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
void namedConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
|
||||
|
||||
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
|
||||
RedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -229,6 +230,17 @@ class RedisHttpSessionConfigurationTests {
|
||||
.withMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customIndexResolverConfiguration() {
|
||||
registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class);
|
||||
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(indexResolver).isNotNull();
|
||||
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
|
||||
}
|
||||
|
||||
@Test // gh-1252
|
||||
void customRedisMessageListenerContainerConfig() {
|
||||
registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class);
|
||||
@@ -238,6 +250,14 @@ class RedisHttpSessionConfigurationTests {
|
||||
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionRepositoryCustomizer() {
|
||||
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
|
||||
MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
private void registerAndRefresh(Class<?>... annotatedClasses) {
|
||||
this.context.register(annotatedClasses);
|
||||
this.context.refresh();
|
||||
@@ -255,7 +275,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class PropertySourceConfiguration {
|
||||
|
||||
@Bean
|
||||
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
return new PropertySourcesPlaceholderConfigurer();
|
||||
}
|
||||
|
||||
@@ -265,7 +285,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
RedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -281,6 +301,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisHttpSessionConfiguration {
|
||||
|
||||
CustomFlushImmediatelySetLegacyConfiguration() {
|
||||
@@ -297,6 +318,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelyLegacyConfiguration {
|
||||
|
||||
}
|
||||
@@ -335,7 +357,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -347,7 +369,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -359,13 +381,13 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -376,7 +398,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class NamedConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory redisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -387,7 +409,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class MultipleConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -405,15 +427,45 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession
|
||||
static class CustomIndexResolverConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
IndexResolver<Session> indexResolver() {
|
||||
return mock(IndexResolver.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession
|
||||
static class CustomRedisMessageListenerContainerConfig {
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer() {
|
||||
RedisMessageListenerContainer redisMessageListenerContainer() {
|
||||
return new RedisMessageListenerContainer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableRedisHttpSession
|
||||
static class SessionRepositoryCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
return (sessionRepository) -> sessionRepository
|
||||
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -42,7 +42,6 @@ import static org.mockito.Mockito.mock;
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Mark Paluch
|
||||
* @since 1.0.2
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@@ -63,16 +62,15 @@ class Gh109Tests {
|
||||
* override sessionRepository construction to set the custom session-timeout
|
||||
*/
|
||||
@Bean
|
||||
public RedisOperationsSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
|
||||
RedisIndexedSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
|
||||
ApplicationEventPublisher applicationEventPublisher) {
|
||||
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
|
||||
sessionRedisTemplate);
|
||||
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(sessionRedisTemplate);
|
||||
sessionRepository.setDefaultMaxInactiveInterval(this.sessionTimeout);
|
||||
return sessionRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
|
||||
RedisConnection connection = mock(RedisConnection.class);
|
||||
given(factory.getConnection()).willReturn(connection);
|
||||
|
||||
@@ -25,12 +25,14 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -68,8 +70,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void defaultConfiguration() {
|
||||
registerAndRefresh(RedisConfig.class, DefaultConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
}
|
||||
|
||||
@@ -77,8 +78,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void springSessionRedisOperationsResolvingConfiguration() {
|
||||
registerAndRefresh(RedisConfig.class, SpringSessionRedisOperationsResolvingConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
ReactiveRedisOperations<String, Object> springSessionRedisOperations = this.context
|
||||
.getBean(SpringSessionRedisOperationsResolvingConfig.class).getSpringSessionRedisOperations();
|
||||
@@ -91,8 +91,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void customNamespace() {
|
||||
registerAndRefresh(RedisConfig.class, CustomNamespaceConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "namespace")).isEqualTo(REDIS_NAMESPACE + ":");
|
||||
}
|
||||
@@ -101,8 +100,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void customMaxInactiveInterval() {
|
||||
registerAndRefresh(RedisConfig.class, CustomMaxInactiveIntervalConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
|
||||
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
@@ -111,23 +109,22 @@ class RedisWebSessionConfigurationTests {
|
||||
@Test
|
||||
void customSaveModeAnnotation() {
|
||||
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
|
||||
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
|
||||
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
|
||||
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSaveModeSetter() {
|
||||
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
|
||||
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
|
||||
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
|
||||
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void qualifiedConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
|
||||
ReactiveRedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -143,8 +140,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void primaryConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
|
||||
ReactiveRedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -160,8 +156,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
|
||||
ReactiveRedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -177,8 +172,7 @@ class RedisWebSessionConfigurationTests {
|
||||
void namedConnectionFactoryRedisConfig() {
|
||||
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
|
||||
ReactiveRedisConnectionFactory.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -198,12 +192,11 @@ class RedisWebSessionConfigurationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customRedisSerializerConfig() {
|
||||
registerAndRefresh(RedisConfig.class, CustomRedisSerializerConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
RedisSerializer<Object> redisSerializer = this.context.getBean("springSessionDefaultRedisSerializer",
|
||||
RedisSerializer.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -222,6 +215,14 @@ class RedisWebSessionConfigurationTests {
|
||||
"serializer")).isEqualTo(redisSerializer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionRepositoryCustomizer() {
|
||||
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
|
||||
ReactiveRedisSessionRepository sessionRepository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
|
||||
MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
private void registerAndRefresh(Class<?>... annotatedClasses) {
|
||||
this.context.register(annotatedClasses);
|
||||
this.context.refresh();
|
||||
@@ -231,7 +232,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -283,7 +284,7 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -294,7 +295,7 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -305,13 +306,13 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -321,7 +322,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class NamedConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory redisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -331,7 +332,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class MultipleConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -342,10 +343,28 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
return mock(RedisSerializer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableRedisWebSession
|
||||
static class SessionRepositoryCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
return (sessionRepository) -> sessionRepository
|
||||
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,22 @@ dependencies {
|
||||
|
||||
def versions = dependencyManagement.managedVersions
|
||||
|
||||
asciidoctorPdf {
|
||||
clearSources()
|
||||
sources {
|
||||
include "index.adoc"
|
||||
}
|
||||
}
|
||||
|
||||
asciidoctor {
|
||||
clearSources()
|
||||
sources {
|
||||
include "index.adoc"
|
||||
include "guides/*.adoc"
|
||||
}
|
||||
}
|
||||
|
||||
asciidoctorj {
|
||||
def ghTag = snapshotBuild ? 'master' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-session/tree/$ghTag"
|
||||
|
||||
@@ -46,5 +61,7 @@ asciidoctor {
|
||||
'spring-session-version': project.version,
|
||||
'version-milestone': milestoneBuild,
|
||||
'version-release': releaseBuild,
|
||||
'version-snapshot': snapshotBuild
|
||||
'version-snapshot': snapshotBuild,
|
||||
'highlightjsdir@': "js/highlight",
|
||||
'docinfodir@': "."
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
= Spring Session - find by username
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to find sessions by username.
|
||||
|
||||
NOTE: You can find the completed guide in the <<findbyusername-sample, findbyusername application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
[[findbyusername-assumptions]]
|
||||
== Assumptions
|
||||
@@ -23,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.
|
||||
@@ -140,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.
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - Spring Boot
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` when you use Spring Boot.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-boot-sample, httpsession-jdbc-boot sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
@@ -46,6 +52,9 @@ spring.session.store-type=jdbc # Session store type.
|
||||
----
|
||||
====
|
||||
|
||||
If a single Spring Session module is present on the classpath, Spring Boot uses that store implementation automatically.
|
||||
If you have more than one implementation, you must choose the StoreType that you wish to use to store the sessions, as shows above.
|
||||
|
||||
Under the hood, Spring Boot applies configuration that is equivalent to manually adding the `@EnableJdbcHttpSession` annotation.
|
||||
This creates a Spring bean with the name of `springSessionRepositoryFilter`. That bean implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - Spring Boot
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use Spring Boot.
|
||||
|
||||
NOTE: You can find the completed guide in the <<boot-sample, boot sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must ensure your dependencies.
|
||||
@@ -53,7 +59,7 @@ Further customization is possible by using `application.properties`, as the foll
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
|
||||
spring.session.redis.flush-mode=on-save # Sessions flush mode.
|
||||
spring.session.redis.flush-mode=on_save # Sessions flush mode.
|
||||
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
|
||||
----
|
||||
====
|
||||
@@ -151,6 +157,6 @@ To do so, enter the following into your terminal, being sure to replace `7e8383a
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
=====
|
||||
====
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
= Spring Session - WebFlux with Custom Cookie
|
||||
Eleftheria Stein-Kousathana
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to configure Spring Session to use custom cookies in a WebFlux based application.
|
||||
The guide assumes you have already set up Spring Session in your project using your chosen data store. For example, link:./boot-redis.html[HttpSession with Redis].
|
||||
|
||||
NOTE: You can find the completed guide in the <<webflux-custom-cookie-sample, WebFlux Custom Cookie sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
[[webflux-custom-cookie-spring-configuration]]
|
||||
== Spring Boot Configuration
|
||||
|
||||
Once you have set up Spring Session, you can customize how the session cookie is written by exposing a `WebSessionIdResolver` as a Spring bean.
|
||||
Spring Session uses a `CookieWebSessionIdResolver` by default.
|
||||
Exposing the `WebSessionIdResolver` as a Spring bean augments the existing configuration when you use configurations like `@EnableRedisHttpSession`.
|
||||
The following example shows how to customize Spring Session's cookie:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}spring-session-sample-boot-webflux-custom-cookie/src/main/java/sample/CookieConfig.java[tags=webflux-cookie-serializer]
|
||||
----
|
||||
|
||||
<1> We customize the name of the cookie to be `JSESSIONID`.
|
||||
<2> We customize the path of the cookie to be `/` (rather than the default of the context root).
|
||||
<3> We customize the `SameSite` cookie directive to be `Strict`.
|
||||
====
|
||||
|
||||
[[webflux-custom-cookie-sample]]
|
||||
== `webflux-custom-cookie` Sample Application
|
||||
|
||||
This section describes how to work with the `webflux-custom-cookie` sample application.
|
||||
|
||||
=== Running the `webflux-custom-cookie` Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-webflux-custom-cookie:bootRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `webflux-custom-cookie` Sample Application
|
||||
|
||||
Now you can use the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
|
||||
Now click the *Set Attribute* button.
|
||||
You should now see the values displayed in the table.
|
||||
|
||||
If you look at the cookies for the application, you can see the cookie is saved to the custom name of `JSESSIONID`.
|
||||
@@ -1,7 +1,10 @@
|
||||
= Spring Session - WebSocket
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to ensure that WebSocket messages keep your HttpSession alive.
|
||||
|
||||
@@ -12,9 +15,12 @@ Specifically,it does not work with using https://www.jcp.org/en/jsr/detail?id=35
|
||||
|
||||
// end::disclaimer[]
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== HttpSession Setup
|
||||
|
||||
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:httpsession.html[HttpSession Guide].
|
||||
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:./boot-redis.html[HttpSession with Redis Guide].
|
||||
|
||||
Please make sure you have already integrated Spring Session with HttpSession before proceeding.
|
||||
|
||||
@@ -53,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[]
|
||||
@@ -125,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/
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<script type="text/javascript" src="../js/tocbot/tocbot.min.js"></script>
|
||||
<script type="text/javascript" src="../js/toc.js"></script>
|
||||
@@ -1,12 +1,18 @@
|
||||
= Spring Session - Custom Cookie
|
||||
Rob Winch
|
||||
:toc:
|
||||
Rob Winch; Eleftheria Stein-Kousathana
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to configure Spring Session to use custom cookies with Java Configuration.
|
||||
The guide assumes you have already link:./httpsession.html[set up Spring Session in your project].
|
||||
The guide assumes you have already set up Spring Session in your project using your chosen data store. For example, link:./boot-redis.html[HttpSession with Redis].
|
||||
|
||||
NOTE: You can find the completed guide in the <<custom-cookie-sample, Custom Cookie sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
[[custom-cookie-spring-configuration]]
|
||||
== Spring Java Configuration
|
||||
|
||||
@@ -58,6 +64,9 @@ See `domainNamePattern` as an alternative.
|
||||
The pattern should provide a single grouping that is used to extract the value of the cookie domain.
|
||||
If the regular expression does not match, no domain is set and the existing domain is used.
|
||||
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
|
||||
* `sameSite`: The value for the `SameSite` cookie directive.
|
||||
To disable the serialization of the `SameSite` cookie directive, you may set this value to `null`.
|
||||
Default: `Lax`
|
||||
|
||||
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
|
||||
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
= Spring Session and Spring Security with Hazelcast
|
||||
Tommy Ludwig; Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session along with Spring Security when you use Hazelcast as your data store.
|
||||
It assumes that you have already applied Spring Security to your application.
|
||||
|
||||
NOTE: You cand find the completed guide in the <<hazelcast-spring-security-sample, Hazelcast Spring Security sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
@@ -91,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`.
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - HttpSession (Quick Start)
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` with Java Configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-sample, httpsession-jdbc sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
@@ -99,7 +105,7 @@ For additional information on how to configure data access related concerns, see
|
||||
|
||||
== Java Servlet Container Initialization
|
||||
|
||||
Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<httpsession-jdbc-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
@@ -122,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]]
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
= Spring Session - HttpSession (Quick Start)
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:version-snapshot: true
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with Java Configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-sample, httpsession sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you are using Maven, you must add the following dependencies:
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - REST
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use REST endpoints.
|
||||
|
||||
NOTE: You can find the completed guide in the <<rest-sample, rest sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
@@ -241,7 +247,7 @@ $ curl -v http://localhost:8080/ -u user:password
|
||||
|
||||
In the output, you should notice the following:
|
||||
|
||||
===
|
||||
====
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
= Spring Session and Spring Security
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session along with Spring Security.
|
||||
It assumes you have already applied Spring Security to your application.
|
||||
|
||||
NOTE: You can find the completed guide in the <<security-sample, security sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - HttpSession (Quick Start)
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational to back a web application's `HttpSession` with XML based configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-xml-sample, httpsession-jdbc-xml sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
@@ -68,8 +74,8 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
endif::[]
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
|
||||
@@ -101,7 +107,7 @@ For additional information on how to configure data access-related concerns, see
|
||||
|
||||
== XML Servlet Container Initialization
|
||||
|
||||
Our <<httpsession-xml-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<httpsession-jdbc-xml-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, we need to instruct Spring to load our `session.xml` configuration.
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
= Spring Session - HttpSession (Quick Start)
|
||||
Rob Winch
|
||||
:toc:
|
||||
:toc: left
|
||||
:stylesdir: ../
|
||||
:highlightjsdir: ../js/highlight
|
||||
:docinfodir: guides
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with XML-based configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-xml-sample, httpsession-xml sample application>>.
|
||||
|
||||
[#index-link]
|
||||
link:../index.html[Index]
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= Spring Session
|
||||
Rob Winch; Vedran Pavić; Jay Bryant
|
||||
Rob Winch; Vedran Pavić; Jay Bryant; Eleftheria Stein-Kousathana
|
||||
:doctype: book
|
||||
:indexdoc-tests: {docs-test-dir}docs/IndexDocTests.java
|
||||
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
|
||||
@@ -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]
|
||||
@@ -70,12 +74,16 @@ To get started with Spring Session, the best place to start is our Sample Applic
|
||||
| Demonstrates how to use Spring Session to replace the Spring WebFlux's `WebSession` with Redis.
|
||||
|
|
||||
|
||||
| {gh-samples-url}spring-session-sample-boot-webflux-custom-cookie[WebFlux with Custom Cookie]
|
||||
| Demonstrates how to use Spring Session to customize the Session cookie in a WebFlux based application.
|
||||
| link:guides/boot-webflux-custom-cookie.html[WebFlux with Custom Cookie Guide]
|
||||
|
||||
| {gh-samples-url}spring-session-sample-boot-redis-json[HttpSession with Redis JSON serialization]
|
||||
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using JSON serialization.
|
||||
|
|
||||
|
||||
| {gh-samples-url}spring-session-sample-boot-redis-simple[HttpSession with simple Redis `SessionRepository`]
|
||||
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `SimpleRedisOperationsSessionRepository`.
|
||||
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `RedisSessionRepository`.
|
||||
|
|
||||
|
||||
|===
|
||||
@@ -289,7 +297,7 @@ Any method that returns an `HttpSession` is overridden.
|
||||
All other methods are implemented by `HttpServletRequestWrapper` and delegate to the original `HttpServletRequest` implementation.
|
||||
|
||||
We replace the `HttpServletRequest` implementation by using a servlet `Filter` called `SessionRepositoryFilter`.
|
||||
The pseudocode belows:
|
||||
The following pseudocode shows how it works:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
@@ -590,12 +598,13 @@ You can browse the complete link:../../api/[Javadoc] online. The key APIs are de
|
||||
* <<api-reactivesessionrepository>>
|
||||
* <<api-enablespringhttpsession>>
|
||||
* <<api-enablespringwebsession>>
|
||||
* <<api-redisoperationssessionrepository>>
|
||||
* <<api-reactiveredisoperationssessionrepository>>
|
||||
* <<api-redisindexedsessionrepository>>
|
||||
* <<api-reactiveredissessionrepository>>
|
||||
* <<api-mapsessionrepository>>
|
||||
* <<api-reactivemapsessionrepository>>
|
||||
* <<api-jdbcoperationssessionrepository>>
|
||||
* <<api-hazelcastsessionrepository>>
|
||||
* <<api-jdbcindexedsessionrepository>>
|
||||
* <<api-hazelcastindexedsessionrepository>>
|
||||
* <<api-cookieserializer>>
|
||||
|
||||
[[api-session]]
|
||||
=== Using `Session`
|
||||
@@ -727,31 +736,31 @@ Note that no infrastructure for session expirations is configured for you.
|
||||
This is because things such as session expiration are highly implementation-dependent.
|
||||
This means that, if you require cleaning up expired sessions, you are responsible for cleaning up the expired sessions.
|
||||
|
||||
[[api-redisoperationssessionrepository]]
|
||||
=== Using `RedisOperationsSessionRepository`
|
||||
[[api-redisindexedsessionrepository]]
|
||||
=== Using `RedisIndexedSessionRepository`
|
||||
|
||||
`RedisOperationsSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
|
||||
`RedisIndexedSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
|
||||
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
|
||||
The implementation supports `SessionDestroyedEvent` and `SessionCreatedEvent` through `SessionMessageListener`.
|
||||
|
||||
[[api-redisoperationssessionrepository-new]]
|
||||
==== Instantiating a `RedisOperationsSessionRepository`
|
||||
[[api-redisindexedsessionrepository-new]]
|
||||
==== Instantiating a `RedisIndexedSessionRepository`
|
||||
|
||||
You can see a typical example of how to create a new instance in the following listing:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{indexdoc-tests}[tags=new-redisoperationssessionrepository]
|
||||
include::{indexdoc-tests}[tags=new-redisindexedsessionrepository]
|
||||
----
|
||||
====
|
||||
|
||||
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
|
||||
|
||||
[[api-redisoperationssessionrepository-config]]
|
||||
[[api-redisindexedsessionrepository-config]]
|
||||
==== Using `@EnableRedisHttpSession`
|
||||
|
||||
In a web environment, the simplest way to create a new `RedisOperationsSessionRepository` is to use `@EnableRedisHttpSession`.
|
||||
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
|
||||
You can find complete example usage in the <<samples>>.
|
||||
You can use the following attributes to customize the configuration:
|
||||
|
||||
@@ -766,11 +775,11 @@ You can customize the serialization by creating a bean named `springSessionDefau
|
||||
|
||||
==== Redis `TaskExecutor`
|
||||
|
||||
`RedisOperationsSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
|
||||
`RedisIndexedSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
|
||||
You can customize the way those events are dispatched by creating a bean named `springSessionRedisTaskExecutor`, a bean `springSessionRedisSubscriptionExecutor`, or both.
|
||||
You can find more details on configuring Redis task executors https://docs.spring.io/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here].
|
||||
|
||||
[[api-redisoperationssessionrepository-storage]]
|
||||
[[api-redisindexedsessionrepository-storage]]
|
||||
==== Storage Details
|
||||
|
||||
The following sections outline how Redis is updated for each operation.
|
||||
@@ -819,10 +828,10 @@ In the preceding example, the following statements are true about the session:
|
||||
The first is `attrName`, with a value of `someAttrValue`.
|
||||
The second session attribute is named `attrName2`, with a value of `someAttrValue2`.
|
||||
|
||||
[[api-redisoperationssessionrepository-writes]]
|
||||
[[api-redisindexedsessionrepository-writes]]
|
||||
===== Optimized Writes
|
||||
|
||||
The `Session` instances managed by `RedisOperationsSessionRepository` keeps track of the properties that have changed and updates only those.
|
||||
The `Session` instances managed by `RedisIndexedSessionRepository` keeps track of the properties that have changed and updates only those.
|
||||
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
|
||||
For example, assume the `sessionAttr2` session attribute from the lsiting in the preceding section was updated.
|
||||
The following command would be run upon saving:
|
||||
@@ -833,7 +842,7 @@ HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:a
|
||||
----
|
||||
====
|
||||
|
||||
[[api-redisoperationssessionrepository-expiration]]
|
||||
[[api-redisindexedsessionrepository-expiration]]
|
||||
===== Session Expiration
|
||||
|
||||
An expiration is associated with each session by using the `EXPIRE` command, based upon the `Session.getMaxInactiveInterval()`.
|
||||
@@ -852,7 +861,7 @@ An expiration is set on the session itself five minutes after it actually expire
|
||||
NOTE: The `SessionRepository.findById(String)` method ensures that no expired sessions are returned.
|
||||
This means that you need not check the expiration before using a session.
|
||||
|
||||
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
|
||||
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
|
||||
`SessionDeletedEvent` or `SessionExpiredEvent` ensure that resources associated with the `Session` are cleaned up.
|
||||
For example, when you use Spring Session's WebSocket support, the Redis expired or delete event triggers any WebSocket connections associated with the session to be closed.
|
||||
|
||||
@@ -893,12 +902,12 @@ Short of using distributed locks (which would kill our performance), there is no
|
||||
By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
|
||||
|
||||
|
||||
[[api-redisoperationssessionrepository-sessiondestroyedevent]]
|
||||
[[api-redisindexedsessionrepository-sessiondestroyedevent]]
|
||||
==== `SessionDeletedEvent` and `SessionExpiredEvent`
|
||||
|
||||
`SessionDeletedEvent` and `SessionExpiredEvent` are both types of `SessionDestroyedEvent`.
|
||||
|
||||
`RedisOperationsSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
|
||||
`RedisIndexedSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
|
||||
This is necessary to ensure resources associated with the `Session` are properly cleaned up.
|
||||
|
||||
For example, when integrating with WebSockets, the `SessionDestroyedEvent` is in charge of closing any active WebSocket connections.
|
||||
@@ -937,15 +946,15 @@ include::{docs-test-resources-dir}docs/HttpSessionConfigurationNoOpConfigureRedi
|
||||
----
|
||||
====
|
||||
|
||||
[[api-redisoperationssessionrepository-sessioncreatedevent]]
|
||||
[[api-redisindexedsessionrepository-sessioncreatedevent]]
|
||||
==== Using `SessionCreatedEvent`
|
||||
|
||||
When a session is created, an event is sent to Redis with a channel ID of `spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe`,
|
||||
where `33fdd1b6-b496-4b33-9f7d-df96679d32fe` is the session ID. The body of the event is the session that was created.
|
||||
|
||||
If registered as a `MessageListener` (the default), `RedisOperationsSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
|
||||
If registered as a `MessageListener` (the default), `RedisIndexedSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
|
||||
|
||||
[[api-redisoperationssessionrepository-cli]]
|
||||
[[api-redisindexedsessionrepository-cli]]
|
||||
==== Viewing the Session in Redis
|
||||
|
||||
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
|
||||
@@ -980,30 +989,30 @@ redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed
|
||||
----
|
||||
====
|
||||
|
||||
[[api-reactiveredisoperationssessionrepository]]
|
||||
=== Using `ReactiveRedisOperationsSessionRepository`
|
||||
[[api-reactiveredissessionrepository]]
|
||||
=== Using `ReactiveRedisSessionRepository`
|
||||
|
||||
`ReactiveRedisOperationsSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
|
||||
`ReactiveRedisSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
|
||||
In a web environment, this is typically used in combination with `WebSessionStore`.
|
||||
|
||||
[[api-reactiveredisoperationssessionrepository-new]]
|
||||
==== Instantiating a `ReactiveRedisOperationsSessionRepository`
|
||||
[[api-reactiveredissessionrepository-new]]
|
||||
==== Instantiating a `ReactiveRedisSessionRepository`
|
||||
|
||||
The following example shows how to create a new instance:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{indexdoc-tests}[tags=new-reactiveredisoperationssessionrepository]
|
||||
include::{indexdoc-tests}[tags=new-reactiveredissessionrepository]
|
||||
----
|
||||
====
|
||||
|
||||
For additional information on how to create a `ReactiveRedisConnectionFactory`, see the Spring Data Redis Reference.
|
||||
|
||||
[[api-reactiveredisoperationssessionrepository-config]]
|
||||
[[api-reactiveredissessionrepository-config]]
|
||||
==== Using `@EnableRedisWebSession`
|
||||
|
||||
In a web environment, the simplest way to create a new `ReactiveRedisOperationsSessionRepository` is to use `@EnableRedisWebSession`.
|
||||
In a web environment, the simplest way to create a new `ReactiveRedisSessionRepository` is to use `@EnableRedisWebSession`.
|
||||
You can use the following attributes to customize the configuration:
|
||||
|
||||
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds
|
||||
@@ -1011,13 +1020,13 @@ You can use the following attributes to customize the configuration:
|
||||
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `ReactiveSessionRepository`.
|
||||
A value of `FlushMode.IMMEDIATE` writes to Redis as soon as possible.
|
||||
|
||||
[[api-reactiveredisoperationssessionrepository-writes]]
|
||||
[[api-reactiveredissessionrepository-writes]]
|
||||
===== Optimized Writes
|
||||
|
||||
The `Session` instances managed by `ReactiveRedisOperationsSessionRepository` keep track of the properties that have changed and updates only those.
|
||||
The `Session` instances managed by `ReactiveRedisSessionRepository` keep track of the properties that have changed and updates only those.
|
||||
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
|
||||
|
||||
[[api-reactiveredisoperationssessionrepository-cli]]
|
||||
[[api-reactiveredissessionrepository-cli]]
|
||||
==== Viewing the Session in Redis
|
||||
|
||||
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
|
||||
@@ -1101,31 +1110,31 @@ The `ReactiveMapSessionRepository` allows for persisting `Session` in a `Map`, w
|
||||
You can use the implementation with a `ConcurrentHashMap` as a testing or convenience mechanism.
|
||||
Alternatively, you can use it with distributed `Map` implementations, with the requirement that the supplied `Map` must be non-blocking.
|
||||
|
||||
[[api-jdbcoperationssessionrepository]]
|
||||
=== Using `JdbcOperationsSessionRepository`
|
||||
[[api-jdbcindexedsessionrepository]]
|
||||
=== Using `JdbcIndexedSessionRepository`
|
||||
|
||||
`JdbcOperationsSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
|
||||
`JdbcIndexedSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
|
||||
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
|
||||
Note that this implementation does not support publishing of session events.
|
||||
|
||||
[[api-jdbcoperationssessionrepository-new]]
|
||||
==== Instantiating a `JdbcOperationsSessionRepository`
|
||||
[[api-jdbcindexedsessionrepository-new]]
|
||||
==== Instantiating a `JdbcIndexedSessionRepository`
|
||||
|
||||
The following example shows how to create a new instance:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{indexdoc-tests}[tags=new-jdbcoperationssessionrepository]
|
||||
include::{indexdoc-tests}[tags=new-jdbcindexedsessionrepository]
|
||||
----
|
||||
====
|
||||
|
||||
For additional information on how to create and configure `JdbcTemplate` and `PlatformTransactionManager`, see the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
|
||||
|
||||
[[api-jdbcoperationssessionrepository-config]]
|
||||
[[api-jdbcindexedsessionrepository-config]]
|
||||
==== Using `@EnableJdbcHttpSession`
|
||||
|
||||
In a web environment, the simplest way to create a new `JdbcOperationsSessionRepository` is to use `@EnableJdbcHttpSession`.
|
||||
In a web environment, the simplest way to create a new `JdbcIndexedSessionRepository` is to use `@EnableJdbcHttpSession`.
|
||||
You can find complete example usage in the <<samples>>
|
||||
You can use the following attributes to customize the configuration:
|
||||
|
||||
@@ -1142,7 +1151,7 @@ You can customize the default serialization and deserialization of the session b
|
||||
When working in a typical Spring environment, the default `ConversionService` bean (named `conversionService`) is automatically picked up and used for serialization and deserialization.
|
||||
However, you can override the default `ConversionService` by providing a bean named `springSessionConversionService`.
|
||||
|
||||
[[api-jdbcoperationssessionrepository-storage]]
|
||||
[[api-jdbcindexedsessionrepository-storage]]
|
||||
==== Storage Details
|
||||
|
||||
By default, this implementation uses `SPRING_SESSION` and `SPRING_SESSION_ATTRIBUTES` tables to store sessions.
|
||||
@@ -1172,24 +1181,24 @@ include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schem
|
||||
|
||||
==== Transaction Management
|
||||
|
||||
All JDBC operations in `JdbcOperationsSessionRepository` 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-hazelcastsessionrepository]]
|
||||
=== Using `HazelcastSessionRepository`
|
||||
[[api-hazelcastindexedsessionrepository]]
|
||||
=== Using `HazelcastIndexedSessionRepository`
|
||||
|
||||
`HazelcastSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
|
||||
`HazelcastIndexedSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
|
||||
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
|
||||
|
||||
[[api-hazelcastsessionrepository-new]]
|
||||
==== Instantiating a `HazelcastSessionRepository`
|
||||
[[api-hazelcastindexedsessionrepository-new]]
|
||||
==== Instantiating a `HazelcastIndexedSessionRepository`
|
||||
|
||||
The following example shows how to create a new instance:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{indexdoc-tests}[tags=new-hazelcastsessionrepository]
|
||||
include::{indexdoc-tests}[tags=new-hazelcastindexedsessionrepository]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -1230,6 +1239,68 @@ Note that if you use Hazelcast's `MapStore` to persist your sessions `IMap`, the
|
||||
* Reloading triggers `EntryAddedListener` results in `SessionCreatedEvent` being re-published
|
||||
* Reloading uses default TTL for a given `IMap` results in sessions losing their original TTL
|
||||
|
||||
[[api-cookieserializer]]
|
||||
=== Using `CookieSerializer`
|
||||
|
||||
A `CookieSerializer` is responsible for defining how the session cookie is written.
|
||||
Spring Session comes with a default implementation using `DefaultCookieSerializer`.
|
||||
|
||||
[[api-cookieserializer-bean]]
|
||||
==== Exposing `CookieSerializer` as a bean
|
||||
Exposing the `CookieSerializer` as a Spring bean augments the existing configuration when you use configurations like `@EnableRedisHttpSession`.
|
||||
|
||||
The following example shows how to do so:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}spring-session-sample-javaconfig-custom-cookie/src/main/java/sample/Config.java[tags=cookie-serializer]
|
||||
----
|
||||
|
||||
<1> We customize the name of the cookie to be `JSESSIONID`.
|
||||
<2> We customize the path of the cookie to be `/` (rather than the default of the context root).
|
||||
<3> We customize the domain name pattern (a regular expression) to be `^.+?\\.(\\w+\\.[a-z]+)$`.
|
||||
This allows sharing a session across domains and applications.
|
||||
If the regular expression does not match, no domain is set and the existing domain is used.
|
||||
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
|
||||
This means that a request to https://child.example.com sets the domain to `example.com`.
|
||||
However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and, thus, still works in development without any changes being necessary for production.
|
||||
====
|
||||
|
||||
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
|
||||
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
|
||||
|
||||
[[api-cookieserializer-customization]]
|
||||
==== Customizing `CookieSerializer`
|
||||
|
||||
You can customize how the session cookie is written by using any of the following configuration options on the `DefaultCookieSerializer`.
|
||||
|
||||
* `cookieName`: The name of the cookie to use.
|
||||
Default: `SESSION`.
|
||||
* `useSecureCookie`: Specifies whether a secure cookie should be used.
|
||||
Default: Use the value of `HttpServletRequest.isSecure()` at the time of creation.
|
||||
* `cookiePath`: The path of the cookie.
|
||||
Default: The context root.
|
||||
* `cookieMaxAge`: Specifies the max age of the cookie to be set at the time the session is created.
|
||||
Default: `-1`, which indicates the cookie should be removed when the browser is closed.
|
||||
* `jvmRoute`: Specifies a suffix to be appended to the session ID and included in the cookie.
|
||||
Used to identify which JVM to route to for session affinity.
|
||||
With some implementations (that is, Redis) this option provides no performance benefit.
|
||||
However, it can help with tracing logs of a particular user.
|
||||
* `domainName`: Allows specifying a specific domain name to be used for the cookie.
|
||||
This option is simple to understand but often requires a different configuration between development and production environments.
|
||||
See `domainNamePattern` as an alternative.
|
||||
* `domainNamePattern`: A case-insensitive pattern used to extract the domain name from the `HttpServletRequest#getServerName()`.
|
||||
The pattern should provide a single grouping that is used to extract the value of the cookie domain.
|
||||
If the regular expression does not match, no domain is set and the existing domain is used.
|
||||
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] is used as the domain.
|
||||
* `sameSite`: The value for the `SameSite` cookie directive.
|
||||
To disable the serialization of the `SameSite` cookie directive, you may set this value to `null`.
|
||||
Default: `Lax`
|
||||
|
||||
WARNING: You should only match on valid domain characters, since the domain name is reflected in the response.
|
||||
Doing so prevents a malicious user from performing such attacks as https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
|
||||
|
||||
[[custom-sessionrepository]]
|
||||
== Customing `SessionRepository`
|
||||
|
||||
@@ -1351,7 +1422,7 @@ The minimum requirements for Spring Session are:
|
||||
* Java 8+.
|
||||
* If you run in a Servlet Container (not required), Servlet 3.1+.
|
||||
* If you use other Spring libraries (not required), the minimum required version is Spring 5.0.x.
|
||||
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisoperationssessionrepository-expiration,Session Expiration>>
|
||||
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisindexedsessionrepository-expiration,Session Expiration>>
|
||||
* `@EnableHazelcastHttpSession` requires Hazelcast 3.6+. This is necessary to support <<api-enablehazelcasthttpsession-storage,`FindByIndexNameSessionRepository`>>
|
||||
|
||||
NOTE: At its core, Spring Session has a required dependency only on `spring-jcl`.
|
||||
|
||||
@@ -30,19 +30,18 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.MapSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository;
|
||||
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -120,32 +119,31 @@ class IndexDocTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newRedisOperationsSessionRepository() {
|
||||
// tag::new-redisoperationssessionrepository[]
|
||||
void newRedisIndexedSessionRepository() {
|
||||
// tag::new-redisindexedsessionrepository[]
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
|
||||
// ... configure redisTemplate ...
|
||||
|
||||
SessionRepository<? extends Session> repository = new RedisOperationsSessionRepository(redisTemplate);
|
||||
// end::new-redisoperationssessionrepository[]
|
||||
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
|
||||
// end::new-redisindexedsessionrepository[]
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newReactiveRedisOperationsSessionRepository() {
|
||||
void newReactiveRedisSessionRepository() {
|
||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
|
||||
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
|
||||
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer()).build();
|
||||
|
||||
// tag::new-reactiveredisoperationssessionrepository[]
|
||||
// tag::new-reactiveredissessionrepository[]
|
||||
// ... create and configure connectionFactory and serializationContext ...
|
||||
|
||||
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
|
||||
serializationContext);
|
||||
|
||||
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisOperationsSessionRepository(
|
||||
redisTemplate);
|
||||
// end::new-reactiveredisoperationssessionrepository[]
|
||||
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
|
||||
// end::new-reactiveredissessionrepository[]
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,25 +156,25 @@ class IndexDocTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newJdbcOperationsSessionRepository() {
|
||||
// tag::new-jdbcoperationssessionrepository[]
|
||||
void newJdbcIndexedSessionRepository() {
|
||||
// tag::new-jdbcindexedsessionrepository[]
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate();
|
||||
|
||||
// ... configure JdbcTemplate ...
|
||||
// ... configure jdbcTemplate ...
|
||||
|
||||
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate();
|
||||
|
||||
// ... configure transactionManager ...
|
||||
// ... configure transactionTemplate ...
|
||||
|
||||
SessionRepository<? extends Session> repository = new JdbcOperationsSessionRepository(jdbcTemplate,
|
||||
transactionManager);
|
||||
// end::new-jdbcoperationssessionrepository[]
|
||||
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
|
||||
transactionTemplate);
|
||||
// end::new-jdbcindexedsessionrepository[]
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newHazelcastSessionRepository() {
|
||||
// tag::new-hazelcastsessionrepository[]
|
||||
void newHazelcastIndexedSessionRepository() {
|
||||
// tag::new-hazelcastindexedsessionrepository[]
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
@@ -184,8 +182,8 @@ class IndexDocTests {
|
||||
|
||||
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
|
||||
|
||||
HazelcastSessionRepository repository = new HazelcastSessionRepository(hazelcastInstance);
|
||||
// end::new-hazelcastsessionrepository[]
|
||||
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
|
||||
// end::new-hazelcastindexedsessionrepository[]
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -48,13 +48,13 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
|
||||
|
||||
// tag::configure-redis-action[]
|
||||
@Bean
|
||||
public static ConfigureRedisAction configureRedisAction() {
|
||||
ConfigureRedisAction configureRedisAction() {
|
||||
return ConfigureRedisAction.NO_OP;
|
||||
}
|
||||
// end::configure-redis-action[]
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory redisConnectionFactory() {
|
||||
return mock(RedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.hazelcast.HazelcastSessionRepository;
|
||||
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;
|
||||
|
||||
@@ -35,17 +38,17 @@ public class HazelcastHttpSessionConfig {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance hazelcastInstance() {
|
||||
MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
.setExtractor(PrincipalNameExtractor.class.getName());
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
|
||||
.addMapAttributeConfig(attributeConfig)
|
||||
.addMapIndexConfig(new MapIndexConfig(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
|
||||
return Hazelcast.newHazelcastInstance(config); // <3>
|
||||
MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
.setExtractor(PrincipalNameExtractor.class.getName());
|
||||
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
|
||||
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
||||
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
SerializerConfig serializerConfig = new SerializerConfig();
|
||||
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
|
||||
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
|
||||
return Hazelcast.newHazelcastInstance(config); // <4>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ class HttpSessionListenerJavaConfigTests extends AbstractHttpSessionListenerTest
|
||||
static class MockConfig {
|
||||
|
||||
@Bean
|
||||
public static RedisConnectionFactory redisConnectionFactory() {
|
||||
static RedisConnectionFactory redisConnectionFactory() {
|
||||
return AbstractHttpSessionListenerTests.createMockRedisConnection();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecuritySessionDestroyedListener securitySessionDestroyedListener() {
|
||||
SecuritySessionDestroyedListener securitySessionDestroyedListener() {
|
||||
return new SecuritySessionDestroyedListener();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package docs.security;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
@@ -41,14 +42,16 @@ public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ... additional configuration ...
|
||||
.rememberMe()
|
||||
.rememberMeServices(rememberMeServices());
|
||||
.rememberMe((rememberMe) -> rememberMe
|
||||
.rememberMeServices(rememberMeServices())
|
||||
);
|
||||
// end::http-rememberme[]
|
||||
|
||||
http
|
||||
.formLogin().and()
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated();
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
}
|
||||
|
||||
// tag::rememberme-bean[]
|
||||
|
||||
@@ -40,9 +40,10 @@ public class SecurityConfiguration<S extends Session> extends WebSecurityConfigu
|
||||
// @formatter:off
|
||||
http
|
||||
// other config goes here...
|
||||
.sessionManagement()
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.maximumSessions(2)
|
||||
.sessionRegistry(sessionRegistry());
|
||||
.sessionRegistry(sessionRegistry())
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
48
spring-session-hazelcast/hazelcast4/hazelcast4.gradle
Normal file
48
spring-session-hazelcast/hazelcast4/hazelcast4.gradle
Normal 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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user