Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
478160e5b2 | ||
|
|
2ef91fa7a2 | ||
|
|
8cd185db6b | ||
|
|
6fbd11e893 | ||
|
|
401bff267c | ||
|
|
7805aff531 | ||
|
|
7d5ba54289 | ||
|
|
45e6c3a0ba | ||
|
|
9f142ed49d | ||
|
|
0758dd737d | ||
|
|
f7852f379a | ||
|
|
61d3d56e6d | ||
|
|
7d929457e4 | ||
|
|
3d266960ba | ||
|
|
4d96099e03 | ||
|
|
e0103f62d6 | ||
|
|
304b496caf | ||
|
|
c1e3c2831d | ||
|
|
e228279683 | ||
|
|
70c14368e5 | ||
|
|
7f9abc822f | ||
|
|
f426f91574 | ||
|
|
316fe09f72 | ||
|
|
00338e23dd | ||
|
|
c6c2d53204 | ||
|
|
bf5dcda905 | ||
|
|
32ec8b2b28 | ||
|
|
3268d1f790 | ||
|
|
380a1e81ac | ||
|
|
5be4141103 | ||
|
|
4ebf18dc4e | ||
|
|
b5c67736ad | ||
|
|
dfab409f30 | ||
|
|
a0a394d17f | ||
|
|
1a98f25fdb | ||
|
|
1afb5d5a17 | ||
|
|
365a244a9b | ||
|
|
0b4140d892 | ||
|
|
78a85789c9 | ||
|
|
59350ed559 | ||
|
|
811e156a9c | ||
|
|
05a9903348 | ||
|
|
d8ae336b24 | ||
|
|
315112f2a2 | ||
|
|
e859da6d27 | ||
|
|
028bae1f11 | ||
|
|
234cb6dd88 | ||
|
|
43101308ec | ||
|
|
089f6b92de | ||
|
|
c6d129a5a5 | ||
|
|
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 |
18
.travis.yml
18
.travis.yml
@@ -1,20 +1,16 @@
|
||||
language: java
|
||||
|
||||
sudo: required
|
||||
|
||||
services: docker
|
||||
|
||||
jdk: oraclejdk8
|
||||
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk8
|
||||
- openjdk11
|
||||
services:
|
||||
- docker
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
install: true
|
||||
|
||||
script: ./gradlew clean build --refresh-dependencies --no-daemon
|
||||
script: ./gradlew clean check --no-daemon --stacktrace
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
[NOTE]
|
||||
======
|
||||
This branch of Spring Session has reached its https://github.com/spring-projects/spring-boot/wiki/Supported-Versions[End of Life], meaning that there are no further maintenance releases or security patches planned.
|
||||
Please migrate to a supported branch as soon as possible.
|
||||
======
|
||||
|
||||
= Spring Session
|
||||
|
||||
image:https://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"]
|
||||
|
||||
26
build.gradle
26
build.gradle
@@ -4,7 +4,7 @@ buildscript {
|
||||
snapshotBuild = version.endsWith('SNAPSHOT')
|
||||
milestoneBuild = !(releaseBuild || snapshotBuild)
|
||||
|
||||
springBootVersion = '2.2.0.M5'
|
||||
springBootVersion = '2.2.13.RELEASE'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -13,7 +13,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.26.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.27.1.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,25 @@ subprojects {
|
||||
|
||||
plugins.withType(JavaPlugin) {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty('artifactoryUsername')) {
|
||||
allprojects { project ->
|
||||
project.repositories { repos ->
|
||||
all { repo ->
|
||||
if (!repo.url.toString().startsWith("https://repo.spring.io/")) {
|
||||
return;
|
||||
}
|
||||
repo.credentials {
|
||||
username = artifactoryUsername
|
||||
password = artifactoryPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.M4
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
version=2.2.7.BUILD-SNAPSHOT
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-RC1'
|
||||
mavenBom 'org.junit:junit-bom:5.5.1'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RC2'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-RC3'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.RC1'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.12.0'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-SR19'
|
||||
mavenBom 'org.junit:junit-bom:5.5.2'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.2.14.RELEASE'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-SR13'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.2.10.RELEASE'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.15.3'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.12.2') {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.12.12') {
|
||||
entry 'hazelcast'
|
||||
entry 'hazelcast-client'
|
||||
}
|
||||
|
||||
dependency 'com.h2database:h2:1.4.199'
|
||||
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.ojdbc:ojdbc8:19.3.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:5.2.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.12'
|
||||
dependency 'mysql:mysql-connector-java:8.0.23'
|
||||
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.hsqldb:hsqldb:2.5.1'
|
||||
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.4'
|
||||
dependency 'org.mockito:mockito-core:3.0.0'
|
||||
dependency 'org.postgresql:postgresql:42.2.6'
|
||||
dependency 'org.postgresql:postgresql:42.2.16'
|
||||
}
|
||||
}
|
||||
|
||||
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.6.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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,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()}.
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -92,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -309,6 +309,10 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
|
||||
&& this.response.isCommitted()) {
|
||||
throw new IllegalStateException("Cannot create a session after the response has been committed");
|
||||
}
|
||||
if (SESSION_LOGGER.isDebugEnabled()) {
|
||||
SESSION_LOGGER.debug(
|
||||
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -423,6 +424,18 @@ class SessionRepositoryFilterTests {
|
||||
assertThat(this.response.getCookie("SESSION")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterGetSessionNewWhenResponseCommittedThenException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> doFilter(new DoInFilter() {
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse)
|
||||
throws IOException {
|
||||
wrappedResponse.getWriter().flush();
|
||||
wrappedRequest.getSession();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterGetSessionNew() throws Exception {
|
||||
doFilter(new DoInFilter() {
|
||||
@@ -1331,11 +1344,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.6";
|
||||
|
||||
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();
|
||||
@@ -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-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.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 sesion 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
|
||||
|
||||
@@ -51,12 +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;
|
||||
@@ -78,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;
|
||||
|
||||
@@ -98,6 +99,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
private RedisConnectionFactory redisConnectionFactory;
|
||||
|
||||
private IndexResolver<Session> indexResolver;
|
||||
|
||||
private RedisSerializer<Object> defaultRedisSerializer;
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@@ -106,17 +109,20 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
private Executor redisSubscriptionExecutor;
|
||||
|
||||
private List<SessionRepositoryCustomizer<RedisOperationsSessionRepository>> sessionRepositoryCustomizers;
|
||||
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);
|
||||
}
|
||||
@@ -135,7 +141,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
|
||||
RedisOperationsSessionRepository sessionRepository) {
|
||||
RedisIndexedSessionRepository sessionRepository) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(this.redisConnectionFactory);
|
||||
if (this.redisTaskExecutor != null) {
|
||||
@@ -216,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) {
|
||||
@@ -230,7 +241,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<SessionRepositoryCustomizer<RedisOperationsSessionRepository>> sessionRepositoryCustomizers) {
|
||||
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -268,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());
|
||||
@@ -295,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,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
|
||||
|
||||
@@ -40,7 +40,7 @@ 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;
|
||||
@@ -63,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;
|
||||
|
||||
@@ -71,17 +71,16 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
private RedisSerializer<Object> defaultRedisSerializer;
|
||||
|
||||
private List<ReactiveSessionRepositoryCustomizer<ReactiveRedisOperationsSessionRepository>> sessionRepositoryCustomizers;
|
||||
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);
|
||||
@@ -129,7 +128,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveRedisOperationsSessionRepository>> sessionRepositoryCustomizers) {
|
||||
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,10 +36,12 @@ 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;
|
||||
|
||||
@@ -56,7 +58,6 @@ 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;
|
||||
@@ -97,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);
|
||||
}
|
||||
@@ -106,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);
|
||||
}
|
||||
@@ -115,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);
|
||||
}
|
||||
@@ -124,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);
|
||||
}
|
||||
@@ -151,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);
|
||||
}
|
||||
|
||||
@@ -166,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();
|
||||
@@ -182,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();
|
||||
@@ -198,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();
|
||||
@@ -214,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();
|
||||
@@ -233,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);
|
||||
@@ -245,8 +253,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void sessionRepositoryCustomizer() {
|
||||
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
|
||||
RedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(RedisOperationsSessionRepository.class);
|
||||
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
|
||||
MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
@@ -268,7 +275,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class PropertySourceConfiguration {
|
||||
|
||||
@Bean
|
||||
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
return new PropertySourcesPlaceholderConfigurer();
|
||||
}
|
||||
|
||||
@@ -278,7 +285,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
RedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -294,6 +301,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisHttpSessionConfiguration {
|
||||
|
||||
CustomFlushImmediatelySetLegacyConfiguration() {
|
||||
@@ -310,6 +318,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelyLegacyConfiguration {
|
||||
|
||||
}
|
||||
@@ -348,7 +357,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -360,7 +369,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -372,13 +381,13 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
RedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -389,7 +398,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class NamedConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisConnectionFactory redisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -400,7 +409,7 @@ class RedisHttpSessionConfigurationTests {
|
||||
static class MultipleConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
RedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
return mockRedisConnectionFactory();
|
||||
}
|
||||
|
||||
@@ -418,12 +427,24 @@ 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();
|
||||
}
|
||||
|
||||
@@ -434,13 +455,13 @@ class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public SessionRepositoryCustomizer<RedisOperationsSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SessionRepositoryCustomizer<RedisOperationsSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
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);
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
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;
|
||||
@@ -70,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();
|
||||
}
|
||||
|
||||
@@ -79,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();
|
||||
@@ -93,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 + ":");
|
||||
}
|
||||
@@ -103,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);
|
||||
@@ -113,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();
|
||||
@@ -145,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();
|
||||
@@ -162,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();
|
||||
@@ -179,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();
|
||||
@@ -200,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();
|
||||
@@ -227,8 +218,7 @@ class RedisWebSessionConfigurationTests {
|
||||
@Test
|
||||
void sessionRepositoryCustomizer() {
|
||||
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
|
||||
ReactiveRedisOperationsSessionRepository sessionRepository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
ReactiveRedisSessionRepository sessionRepository = this.context.getBean(ReactiveRedisSessionRepository.class);
|
||||
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
|
||||
MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
@@ -242,7 +232,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -294,7 +284,7 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionRedisConnectionFactory
|
||||
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -305,7 +295,7 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -316,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);
|
||||
}
|
||||
|
||||
@@ -332,7 +322,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class NamedConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory redisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory redisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -342,7 +332,7 @@ class RedisWebSessionConfigurationTests {
|
||||
static class MultipleConnectionFactoryRedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
|
||||
return mock(ReactiveRedisConnectionFactory.class);
|
||||
}
|
||||
|
||||
@@ -353,7 +343,7 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
return mock(RedisSerializer.class);
|
||||
}
|
||||
|
||||
@@ -364,13 +354,13 @@ class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public ReactiveSessionRepositoryCustomizer<ReactiveRedisOperationsSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public ReactiveSessionRepositoryCustomizer<ReactiveRedisOperationsSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
return (sessionRepository) -> sessionRepository
|
||||
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
@@ -48,3 +48,9 @@ asciidoctor {
|
||||
'version-release': releaseBuild,
|
||||
'version-snapshot': snapshotBuild
|
||||
}
|
||||
|
||||
remotes {
|
||||
docs {
|
||||
host = "docs-ip.spring.io"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= Spring Session - Custom Cookie
|
||||
Rob Winch
|
||||
Rob Winch; Eleftheria Stein-Kousathana
|
||||
:toc:
|
||||
|
||||
This guide describes how to configure Spring Session to use custom cookies with Java Configuration.
|
||||
@@ -58,6 +58,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,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/
|
||||
@@ -75,7 +75,7 @@ To get started with Spring Session, the best place to start is our Sample Applic
|
||||
|
|
||||
|
||||
| {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`.
|
||||
|
|
||||
|
||||
|===
|
||||
@@ -590,12 +590,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 +728,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 +767,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 +820,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 +834,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 +853,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 +894,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 +938,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 +981,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 +1012,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 +1102,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 +1143,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 +1173,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.
|
||||
All JDBC operations in `JdbcIndexedSessionRepository` are executed in a transactional manner.
|
||||
Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
|
||||
|
||||
[[api-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 +1231,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 +1414,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`.
|
||||
|
||||
@@ -36,10 +36,10 @@ 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.support.TransactionTemplate;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
@@ -119,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
|
||||
@@ -157,8 +156,8 @@ class IndexDocTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newJdbcOperationsSessionRepository() {
|
||||
// tag::new-jdbcoperationssessionrepository[]
|
||||
void newJdbcIndexedSessionRepository() {
|
||||
// tag::new-jdbcindexedsessionrepository[]
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate();
|
||||
|
||||
// ... configure jdbcTemplate ...
|
||||
@@ -167,15 +166,15 @@ class IndexDocTests {
|
||||
|
||||
// ... configure transactionTemplate ...
|
||||
|
||||
SessionRepository<? extends Session> repository = new JdbcOperationsSessionRepository(jdbcTemplate,
|
||||
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
|
||||
transactionTemplate);
|
||||
// end::new-jdbcoperationssessionrepository[]
|
||||
// end::new-jdbcindexedsessionrepository[]
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
void newHazelcastSessionRepository() {
|
||||
// tag::new-hazelcastsessionrepository[]
|
||||
void newHazelcastIndexedSessionRepository() {
|
||||
// tag::new-hazelcastindexedsessionrepository[]
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
@@ -183,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ 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.hazelcast.HazelcastIndexedSessionRepository;
|
||||
import org.springframework.session.hazelcast.PrincipalNameExtractor;
|
||||
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
|
||||
|
||||
@@ -35,16 +35,13 @@ 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));
|
||||
|
||||
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));
|
||||
return Hazelcast.newHazelcastInstance(config); // <3>
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,17 +30,17 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Abstract base class for Hazelcast integration tests.
|
||||
* Base class for {@link HazelcastIndexedSessionRepository} integration tests.
|
||||
*
|
||||
* @author Tommy Ludwig
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
abstract class AbstractHazelcastRepositoryITests {
|
||||
abstract class AbstractHazelcastIndexedSessionRepositoryITests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract class AbstractHazelcastRepositoryITests {
|
||||
private HazelcastInstance hazelcastInstance;
|
||||
|
||||
@Autowired
|
||||
private HazelcastSessionRepository repository;
|
||||
private HazelcastIndexedSessionRepository repository;
|
||||
|
||||
@Test
|
||||
void createAndDestroySession() {
|
||||
@@ -56,7 +56,7 @@ abstract class AbstractHazelcastRepositoryITests {
|
||||
String sessionId = sessionToSave.getId();
|
||||
|
||||
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
|
||||
.getMap(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME);
|
||||
.getMap(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
|
||||
|
||||
assertThat(hazelcastMap.size()).isEqualTo(0);
|
||||
|
||||
@@ -22,8 +22,8 @@ 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.BindMode;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -35,22 +35,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests that check the underlying data source - in this case Hazelcast
|
||||
* Client.
|
||||
* Integration tests for {@link HazelcastIndexedSessionRepository} using client-server
|
||||
* topology.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Artem Bilan
|
||||
* @since 1.1
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
|
||||
class ClientServerHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
|
||||
|
||||
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.2")
|
||||
.withExposedPorts(5701).withEnv("JAVA_OPTS", "-Dhazelcast.config=/opt/hazelcast/config_ext/hazelcast.xml")
|
||||
.withClasspathResourceMapping("/hazelcast-server.xml", "/opt/hazelcast/config_ext/hazelcast.xml",
|
||||
BindMode.READ_ONLY);
|
||||
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.12")
|
||||
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
|
||||
"/opt/hazelcast/hazelcast.xml");
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() {
|
||||
@@ -67,7 +65,7 @@ class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests
|
||||
static class HazelcastSessionConfig {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance hazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance() {
|
||||
ClientConfig clientConfig = new ClientConfig();
|
||||
clientConfig.getNetworkConfig()
|
||||
.addAddress(container.getContainerIpAddress() + ":" + container.getFirstMappedPort());
|
||||
@@ -27,8 +27,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests that check the underlying data source - in this case Hazelcast
|
||||
* Server.
|
||||
* Integration tests for {@link HazelcastIndexedSessionRepository} using embedded
|
||||
* topology.
|
||||
*
|
||||
* @author Tommy Ludwig
|
||||
* @author Vedran Pavic
|
||||
@@ -36,14 +36,14 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class HazelcastServerRepositoryITests extends AbstractHazelcastRepositoryITests {
|
||||
class EmbeddedHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
|
||||
|
||||
@EnableHazelcastHttpSession
|
||||
@Configuration
|
||||
static class HazelcastSessionConfig {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance hazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance() {
|
||||
return HazelcastITestUtils.embeddedHazelcastServer();
|
||||
}
|
||||
|
||||
@@ -23,48 +23,32 @@ import com.hazelcast.config.NetworkConfig;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
|
||||
import org.springframework.util.SocketUtils;
|
||||
|
||||
/**
|
||||
* Utility class for Hazelcast integration tests.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public final class HazelcastITestUtils {
|
||||
final class HazelcastITestUtils {
|
||||
|
||||
private HazelcastITestUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link HazelcastInstance} for use in integration tests.
|
||||
* @param port the port for Hazelcast to bind to
|
||||
* @return the Hazelcast instance
|
||||
*/
|
||||
public static HazelcastInstance embeddedHazelcastServer(int port) {
|
||||
MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
.setExtractor(PrincipalNameExtractor.class.getName());
|
||||
|
||||
static HazelcastInstance embeddedHazelcastServer() {
|
||||
Config config = new Config();
|
||||
|
||||
NetworkConfig networkConfig = config.getNetworkConfig();
|
||||
|
||||
networkConfig.setPort(port);
|
||||
|
||||
networkConfig.setPort(0);
|
||||
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
|
||||
|
||||
config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME).addMapAttributeConfig(attributeConfig)
|
||||
.addMapIndexConfig(new MapIndexConfig(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
|
||||
MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
.setExtractor(PrincipalNameExtractor.class.getName());
|
||||
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
|
||||
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
||||
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
return Hazelcast.newHazelcastInstance(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link HazelcastInstance} for use in integration tests.
|
||||
* @return the Hazelcast instance
|
||||
*/
|
||||
public static HazelcastInstance embeddedHazelcastServer() {
|
||||
return embeddedHazelcastServer(SocketUtils.findAvailableTcpPort());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.hazelcast.config.annotation.web.http;
|
||||
package org.springframework.session.hazelcast;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -38,8 +38,7 @@ 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.HazelcastITestUtils;
|
||||
import org.springframework.session.hazelcast.SessionEventRegistry;
|
||||
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;
|
||||
@@ -57,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
class SessionEventHazelcastIndexedSessionRepositoryTests<S extends Session> {
|
||||
|
||||
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
|
||||
|
||||
@@ -206,12 +205,12 @@ class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
static class HazelcastSessionConfig {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance embeddedHazelcast() {
|
||||
HazelcastInstance embeddedHazelcast() {
|
||||
return HazelcastITestUtils.embeddedHazelcastServer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionEventRegistry sessionEventRegistry() {
|
||||
SessionEventRegistry sessionEventRegistry() {
|
||||
return new SessionEventRegistry();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentMap;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
|
||||
public class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
|
||||
class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
|
||||
|
||||
private Map<String, AbstractSessionEvent> events = new HashMap<>();
|
||||
|
||||
@@ -40,17 +40,17 @@ public class SessionEventRegistry implements ApplicationListener<AbstractSession
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
void clear() {
|
||||
this.events.clear();
|
||||
this.locks.clear();
|
||||
}
|
||||
|
||||
public boolean receivedEvent(String sessionId) throws InterruptedException {
|
||||
boolean receivedEvent(String sessionId) throws InterruptedException {
|
||||
return waitForEvent(sessionId) != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
|
||||
<E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
|
||||
return (E) waitForEvent(sessionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* 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.hazelcast.config.annotation.web.http;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import com.hazelcast.config.ClasspathXmlConfig;
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.config.NetworkConfig;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.util.SocketUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test the different configuration options for the {@link EnableHazelcastHttpSession}
|
||||
* annotation.
|
||||
*
|
||||
* @author Tommy Ludwig
|
||||
*/
|
||||
public class HazelcastHttpSessionConfigurationXmlTests<S extends Session> {
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
static class CustomXmlMapNameTest<S extends Session> {
|
||||
|
||||
@Autowired
|
||||
private SessionRepository<S> repository;
|
||||
|
||||
@Test
|
||||
void saveSessionTest() {
|
||||
|
||||
S sessionToSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(sessionToSave);
|
||||
|
||||
S session = this.repository.findById(sessionToSave.getId());
|
||||
|
||||
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(30));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableHazelcastHttpSession(sessionMapName = "my-sessions")
|
||||
static class HazelcastSessionXmlConfigCustomMapName {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance embeddedHazelcast() {
|
||||
Config hazelcastConfig = new ClasspathXmlConfig(
|
||||
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-map-name.xml");
|
||||
NetworkConfig netConfig = new NetworkConfig();
|
||||
netConfig.setPort(SocketUtils.findAvailableTcpPort());
|
||||
hazelcastConfig.setNetworkConfig(netConfig);
|
||||
return Hazelcast.newHazelcastInstance(hazelcastConfig);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
static class CustomXmlMapNameAndIdleTest<S extends Session> {
|
||||
|
||||
@Autowired
|
||||
private SessionRepository<S> repository;
|
||||
|
||||
@Test
|
||||
void saveSessionTest() {
|
||||
|
||||
S sessionToSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(sessionToSave);
|
||||
|
||||
S session = this.repository.findById(sessionToSave.getId());
|
||||
|
||||
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(20));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableHazelcastHttpSession(sessionMapName = "test-sessions", maxInactiveIntervalInSeconds = 1200)
|
||||
static class HazelcastSessionXmlConfigCustomMapNameAndIdle {
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance embeddedHazelcast() {
|
||||
Config hazelcastConfig = new ClasspathXmlConfig(
|
||||
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-idle-time-map-name.xml");
|
||||
NetworkConfig netConfig = new NetworkConfig();
|
||||
netConfig.setPort(SocketUtils.findAvailableTcpPort());
|
||||
hazelcastConfig.setNetworkConfig(netConfig);
|
||||
return Hazelcast.newHazelcastInstance(hazelcastConfig);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.12.xsd">
|
||||
|
||||
<network>
|
||||
<join>
|
||||
<multicast enabled="false"/>
|
||||
</join>
|
||||
</network>
|
||||
|
||||
<user-code-deployment enabled="true">
|
||||
<class-cache-mode>ETERNAL</class-cache-mode>
|
||||
<provider-mode>LOCAL_AND_CACHED_CLASSES</provider-mode>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.12.xsd">
|
||||
|
||||
<group>
|
||||
<name>spring-session-it-test-idle-time-map-name</name>
|
||||
<password>test-pass</password>
|
||||
</group>
|
||||
|
||||
<network>
|
||||
<port auto-increment="true" port-count="100">5701</port>
|
||||
<outbound-ports>
|
||||
<ports>0</ports>
|
||||
</outbound-ports>
|
||||
<join>
|
||||
<multicast enabled="false"/>
|
||||
</join>
|
||||
</network>
|
||||
|
||||
<map name="test-sessions">
|
||||
<in-memory-format>BINARY</in-memory-format>
|
||||
<backup-count>1</backup-count>
|
||||
<async-backup-count>0</async-backup-count>
|
||||
<time-to-live-seconds>0</time-to-live-seconds>
|
||||
<max-idle-seconds>300</max-idle-seconds>
|
||||
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy</merge-policy>
|
||||
<attributes>
|
||||
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
|
||||
</attributes>
|
||||
<indexes>
|
||||
<index ordered="false">principalName</index>
|
||||
</indexes>
|
||||
</map>
|
||||
|
||||
</hazelcast>
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.12.xsd">
|
||||
|
||||
<group>
|
||||
<name>spring-session-it-test-map-name</name>
|
||||
<password>test-pass</password>
|
||||
</group>
|
||||
|
||||
<network>
|
||||
<port auto-increment="true" port-count="100">5701</port>
|
||||
<outbound-ports>
|
||||
<ports>0</ports>
|
||||
</outbound-ports>
|
||||
<join>
|
||||
<multicast enabled="false"/>
|
||||
</join>
|
||||
</network>
|
||||
|
||||
<map name="my-sessions">
|
||||
<in-memory-format>BINARY</in-memory-format>
|
||||
<backup-count>1</backup-count>
|
||||
<async-backup-count>0</async-backup-count>
|
||||
<time-to-live-seconds>0</time-to-live-seconds>
|
||||
<max-idle-seconds>0</max-idle-seconds>
|
||||
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy</merge-policy>
|
||||
<attributes>
|
||||
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
|
||||
</attributes>
|
||||
<indexes>
|
||||
<index ordered="false">principalName</index>
|
||||
</indexes>
|
||||
</map>
|
||||
|
||||
</hazelcast>
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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.hazelcast;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.hazelcast.core.EntryEvent;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
import com.hazelcast.core.IMap;
|
||||
import com.hazelcast.map.listener.EntryAddedListener;
|
||||
import com.hazelcast.map.listener.EntryEvictedListener;
|
||||
import com.hazelcast.map.listener.EntryRemovedListener;
|
||||
import com.hazelcast.query.Predicates;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.session.DelegatingIndexResolver;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.IndexResolver;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.PrincipalNameIndexResolver;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.session.SessionRepository} implementation that stores
|
||||
* sessions in Hazelcast's distributed {@link IMap}.
|
||||
*
|
||||
* <p>
|
||||
* An example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* Config config = new Config();
|
||||
*
|
||||
* // ... configure Hazelcast ...
|
||||
*
|
||||
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
|
||||
*
|
||||
* HazelcastIndexedSessionRepository sessionRepository =
|
||||
* new HazelcastIndexedSessionRepository(hazelcastInstance);
|
||||
* </pre>
|
||||
*
|
||||
* In order to support finding sessions by principal name using
|
||||
* {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of
|
||||
* {@code IMap} supplied to this implementation is required.
|
||||
*
|
||||
* The following snippet demonstrates how to define required configuration using
|
||||
* programmatic Hazelcast Configuration:
|
||||
*
|
||||
* <pre class="code">
|
||||
* MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
* .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
* .setExtractor(PrincipalNameExtractor.class.getName());
|
||||
*
|
||||
* Config config = new Config();
|
||||
*
|
||||
* config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
|
||||
* .addMapAttributeConfig(attributeConfig)
|
||||
* .addMapIndexConfig(new MapIndexConfig(
|
||||
* HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
*
|
||||
* Hazelcast.newHazelcastInstance(config);
|
||||
* </pre>
|
||||
*
|
||||
* This implementation listens for events on the Hazelcast-backed SessionRepository and
|
||||
* translates those events into the corresponding Spring Session events. Publish the
|
||||
* Spring Session events with the given {@link ApplicationEventPublisher}.
|
||||
*
|
||||
* <ul>
|
||||
* <li>entryAdded - {@link SessionCreatedEvent}</li>
|
||||
* <li>entryEvicted - {@link SessionExpiredEvent}</li>
|
||||
* <li>entryRemoved - {@link SessionDeletedEvent}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Tommy Ludwig
|
||||
* @author Mark Anderson
|
||||
* @author Aleksandar Stojsavljevic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class HazelcastIndexedSessionRepository
|
||||
implements FindByIndexNameSessionRepository<HazelcastIndexedSessionRepository.HazelcastSession>,
|
||||
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
|
||||
EntryRemovedListener<String, MapSession> {
|
||||
|
||||
/**
|
||||
* The default name of map used by Spring Session to store sessions.
|
||||
*/
|
||||
public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
|
||||
|
||||
/**
|
||||
* The principal name custom attribute name.
|
||||
*/
|
||||
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final boolean SUPPORTS_SET_TTL = ClassUtils.hasAtLeastOneMethodWithName(IMap.class, "setTtl");
|
||||
|
||||
private static final Log logger = LogFactory.getLog(HazelcastIndexedSessionRepository.class);
|
||||
|
||||
private final HazelcastInstance hazelcastInstance;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = (event) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override
|
||||
* {@link MapSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
|
||||
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
private IMap<String, MapSession> sessions;
|
||||
|
||||
private String sessionListenerId;
|
||||
|
||||
/**
|
||||
* Create a new {@link HazelcastIndexedSessionRepository} instance.
|
||||
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
|
||||
*/
|
||||
public HazelcastIndexedSessionRepository(HazelcastInstance hazelcastInstance) {
|
||||
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
|
||||
this.hazelcastInstance = hazelcastInstance;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
|
||||
this.sessionListenerId = this.sessions.addEntryListener(this, true);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
this.sessions.removeEntryListener(this.sessionListenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link AbstractSessionEvent session events}. The default is to not publish session
|
||||
* events.
|
||||
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
|
||||
* to publish session events. Cannot be null.
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher cannot be null");
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum inactive interval in seconds between requests before newly created
|
||||
* sessions will be invalidated. A negative time indicates that the session will never
|
||||
* timeout. The default is 1800 (30 minutes).
|
||||
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link IndexResolver} to use.
|
||||
* @param indexResolver the index resolver
|
||||
*/
|
||||
public void setIndexResolver(IndexResolver<Session> indexResolver) {
|
||||
Assert.notNull(indexResolver, "indexResolver cannot be null");
|
||||
this.indexResolver = indexResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of map used to store sessions.
|
||||
* @param sessionMapName the session map name
|
||||
*/
|
||||
public void setSessionMapName(String sessionMapName) {
|
||||
Assert.hasText(sessionMapName, "Map name must not be empty");
|
||||
this.sessionMapName = sessionMapName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Hazelcast flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the new Hazelcast flush mode
|
||||
*/
|
||||
public void setFlushMode(FlushMode flushMode) {
|
||||
Assert.notNull(flushMode, "flushMode cannot be null");
|
||||
this.flushMode = flushMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the save mode.
|
||||
* @param saveMode the save mode
|
||||
*/
|
||||
public void setSaveMode(SaveMode saveMode) {
|
||||
Assert.notNull(saveMode, "saveMode must not be null");
|
||||
this.saveMode = saveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HazelcastSession createSession() {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
HazelcastSession session = new HazelcastSession(cached, true);
|
||||
session.flushImmediateIfNecessary();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(HazelcastSession session) {
|
||||
if (session.isNew) {
|
||||
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
else if (session.sessionIdChanged) {
|
||||
this.sessions.delete(session.originalId);
|
||||
session.originalId = session.getId();
|
||||
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
else if (session.hasChanges()) {
|
||||
SessionUpdateEntryProcessor entryProcessor = new SessionUpdateEntryProcessor();
|
||||
if (session.lastAccessedTimeChanged) {
|
||||
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
|
||||
}
|
||||
if (session.maxInactiveIntervalChanged) {
|
||||
if (SUPPORTS_SET_TTL) {
|
||||
updateTtl(session);
|
||||
}
|
||||
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
|
||||
}
|
||||
if (!session.delta.isEmpty()) {
|
||||
entryProcessor.setDelta(new HashMap<>(session.delta));
|
||||
}
|
||||
this.sessions.executeOnKey(session.getId(), entryProcessor);
|
||||
}
|
||||
session.clearChangeFlags();
|
||||
}
|
||||
|
||||
private void updateTtl(HazelcastSession session) {
|
||||
this.sessions.setTtl(session.getId(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HazelcastSession findById(String id) {
|
||||
MapSession saved = this.sessions.get(id);
|
||||
if (saved == null) {
|
||||
return null;
|
||||
}
|
||||
if (saved.isExpired()) {
|
||||
deleteById(saved.getId());
|
||||
return null;
|
||||
}
|
||||
return new HazelcastSession(saved, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
this.sessions.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, HazelcastSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
|
||||
Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size());
|
||||
for (MapSession session : sessions) {
|
||||
sessionMap.put(session.getId(), new HazelcastSession(session, false));
|
||||
}
|
||||
return sessionMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryAdded(EntryEvent<String, MapSession> event) {
|
||||
MapSession session = event.getValue();
|
||||
if (session.getId().equals(session.getOriginalId())) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session created with id: " + session.getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryEvicted(EntryEvent<String, MapSession> event) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session expired with id: " + event.getOldValue().getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryRemoved(EntryEvent<String, MapSession> event) {
|
||||
MapSession session = event.getOldValue();
|
||||
if (session != null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session deleted with id: " + session.getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track if changes have been made since last save.
|
||||
*
|
||||
* @author Aleksandar Stojsavljevic
|
||||
*/
|
||||
final class HazelcastSession implements Session {
|
||||
|
||||
private final MapSession delegate;
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private boolean sessionIdChanged;
|
||||
|
||||
private boolean lastAccessedTimeChanged;
|
||||
|
||||
private boolean maxInactiveIntervalChanged;
|
||||
|
||||
private String originalId;
|
||||
|
||||
private Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
HazelcastSession(MapSession cached, boolean isNew) {
|
||||
this.delegate = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalId = cached.getId();
|
||||
if (this.isNew || (HazelcastIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames()
|
||||
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.delegate.setLastAccessedTime(lastAccessedTime);
|
||||
this.lastAccessedTimeChanged = true;
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.delegate.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.delegate.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.delegate.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
String newSessionId = this.delegate.changeSessionId();
|
||||
this.sessionIdChanged = true;
|
||||
return newSessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.delegate.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.delegate.setMaxInactiveInterval(interval);
|
||||
this.maxInactiveIntervalChanged = true;
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.delegate.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.delegate.getAttribute(attributeName);
|
||||
if (attributeValue != null
|
||||
&& HazelcastIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(attributeName, attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.delegate.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.delegate.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(attributeName, attributeValue);
|
||||
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
|
||||
Map<String, String> indexes = HazelcastIndexedSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(this);
|
||||
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
|
||||
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
|
||||
}
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
setAttribute(attributeName, null);
|
||||
}
|
||||
|
||||
MapSession getDelegate() {
|
||||
return this.delegate;
|
||||
}
|
||||
|
||||
boolean hasChanges() {
|
||||
return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
|
||||
}
|
||||
|
||||
void clearChangeFlags() {
|
||||
this.isNew = false;
|
||||
this.lastAccessedTimeChanged = false;
|
||||
this.sessionIdChanged = false;
|
||||
this.maxInactiveIntervalChanged = false;
|
||||
this.delta.clear();
|
||||
}
|
||||
|
||||
private void flushImmediateIfNecessary() {
|
||||
if (HazelcastIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
HazelcastIndexedSessionRepository.this.save(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,190 +16,33 @@
|
||||
|
||||
package org.springframework.session.hazelcast;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.hazelcast.core.EntryEvent;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
import com.hazelcast.core.IMap;
|
||||
import com.hazelcast.map.listener.EntryAddedListener;
|
||||
import com.hazelcast.map.listener.EntryEvictedListener;
|
||||
import com.hazelcast.map.listener.EntryRemovedListener;
|
||||
import com.hazelcast.query.Predicates;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.session.DelegatingIndexResolver;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.IndexResolver;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.PrincipalNameIndexResolver;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.session.SessionRepository} implementation that stores
|
||||
* sessions in Hazelcast's distributed {@link IMap}.
|
||||
*
|
||||
* <p>
|
||||
* An example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* Config config = new Config();
|
||||
*
|
||||
* // ... configure Hazelcast ...
|
||||
*
|
||||
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
|
||||
*
|
||||
* HazelcastSessionRepository sessionRepository =
|
||||
* new HazelcastSessionRepository(hazelcastInstance);
|
||||
* </pre>
|
||||
*
|
||||
* In order to support finding sessions by principal name using
|
||||
* {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of
|
||||
* {@code IMap} supplied to this implementation is required.
|
||||
*
|
||||
* The following snippet demonstrates how to define required configuration using
|
||||
* programmatic Hazelcast Configuration:
|
||||
*
|
||||
* <pre class="code">
|
||||
* MapAttributeConfig attributeConfig = new MapAttributeConfig()
|
||||
* .setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
|
||||
* .setExtractor(PrincipalNameExtractor.class.getName());
|
||||
*
|
||||
* Config config = new Config();
|
||||
*
|
||||
* config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME)
|
||||
* .addMapAttributeConfig(attributeConfig)
|
||||
* .addMapIndexConfig(new MapIndexConfig(
|
||||
* HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||
*
|
||||
* Hazelcast.newHazelcastInstance(config);
|
||||
* </pre>
|
||||
*
|
||||
* This implementation listens for events on the Hazelcast-backed SessionRepository and
|
||||
* translates those events into the corresponding Spring Session events. Publish the
|
||||
* Spring Session events with the given {@link ApplicationEventPublisher}.
|
||||
*
|
||||
* <ul>
|
||||
* <li>entryAdded - {@link SessionCreatedEvent}</li>
|
||||
* <li>entryEvicted - {@link SessionExpiredEvent}</li>
|
||||
* <li>entryRemoved - {@link SessionDeletedEvent}</li>
|
||||
* </ul>
|
||||
* This {@link SessionRepository} implementation is kept in order to support migration to
|
||||
* {@link HazelcastIndexedSessionRepository} in a backwards compatible manner.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Tommy Ludwig
|
||||
* @author Mark Anderson
|
||||
* @author Aleksandar Stojsavljevic
|
||||
* @since 1.3.0
|
||||
* @deprecated since 2.2.0 in favor of {@link HazelcastIndexedSessionRepository}
|
||||
*/
|
||||
public class HazelcastSessionRepository
|
||||
implements FindByIndexNameSessionRepository<HazelcastSessionRepository.HazelcastSession>,
|
||||
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
|
||||
EntryRemovedListener<String, MapSession> {
|
||||
@Deprecated
|
||||
public class HazelcastSessionRepository extends HazelcastIndexedSessionRepository {
|
||||
|
||||
/**
|
||||
* The default name of map used by Spring Session to store sessions.
|
||||
* Create a new {@link HazelcastSessionRepository} instance.
|
||||
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
|
||||
* @see HazelcastIndexedSessionRepository#HazelcastIndexedSessionRepository(HazelcastInstance)
|
||||
*/
|
||||
public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
|
||||
|
||||
/**
|
||||
* The principal name custom attribute name.
|
||||
*/
|
||||
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final boolean SUPPORTS_SET_TTL = ClassUtils.hasAtLeastOneMethodWithName(IMap.class, "setTtl");
|
||||
|
||||
private static final Log logger = LogFactory.getLog(HazelcastSessionRepository.class);
|
||||
|
||||
private final HazelcastInstance hazelcastInstance;
|
||||
|
||||
private final IndexResolver<HazelcastSession> indexResolver;
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = (event) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override
|
||||
* {@link MapSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
private IMap<String, MapSession> sessions;
|
||||
|
||||
private String sessionListenerId;
|
||||
|
||||
public HazelcastSessionRepository(HazelcastInstance hazelcastInstance) {
|
||||
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
|
||||
this.hazelcastInstance = hazelcastInstance;
|
||||
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
|
||||
this.sessionListenerId = this.sessions.addEntryListener(this, true);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
this.sessions.removeEntryListener(this.sessionListenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link AbstractSessionEvent session events}. The default is to not publish session
|
||||
* events.
|
||||
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
|
||||
* to publish session events. Cannot be null.
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher cannot be null");
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum inactive interval in seconds between requests before newly created
|
||||
* sessions will be invalidated. A negative time indicates that the session will never
|
||||
* timeout. The default is 1800 (30 minutes).
|
||||
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of map used to store sessions.
|
||||
* @param sessionMapName the session map name
|
||||
*/
|
||||
public void setSessionMapName(String sessionMapName) {
|
||||
Assert.hasText(sessionMapName, "Map name must not be empty");
|
||||
this.sessionMapName = sessionMapName;
|
||||
super(hazelcastInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,262 +57,4 @@ public class HazelcastSessionRepository
|
||||
setFlushMode(hazelcastFlushMode.getFlushMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Hazelcast flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the new Hazelcast flush mode
|
||||
*/
|
||||
public void setFlushMode(FlushMode flushMode) {
|
||||
Assert.notNull(flushMode, "flushMode cannot be null");
|
||||
this.flushMode = flushMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the save mode.
|
||||
* @param saveMode the save mode
|
||||
*/
|
||||
public void setSaveMode(SaveMode saveMode) {
|
||||
Assert.notNull(saveMode, "saveMode must not be null");
|
||||
this.saveMode = saveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HazelcastSession createSession() {
|
||||
MapSession cached = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
HazelcastSession session = new HazelcastSession(cached, true);
|
||||
session.flushImmediateIfNecessary();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(HazelcastSession session) {
|
||||
if (session.isNew) {
|
||||
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
else if (session.sessionIdChanged) {
|
||||
this.sessions.delete(session.originalId);
|
||||
session.originalId = session.getId();
|
||||
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
else if (session.hasChanges()) {
|
||||
SessionUpdateEntryProcessor entryProcessor = new SessionUpdateEntryProcessor();
|
||||
if (session.lastAccessedTimeChanged) {
|
||||
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
|
||||
}
|
||||
if (session.maxInactiveIntervalChanged) {
|
||||
if (SUPPORTS_SET_TTL) {
|
||||
updateTtl(session);
|
||||
}
|
||||
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
|
||||
}
|
||||
if (!session.delta.isEmpty()) {
|
||||
entryProcessor.setDelta(new HashMap<>(session.delta));
|
||||
}
|
||||
this.sessions.executeOnKey(session.getId(), entryProcessor);
|
||||
}
|
||||
session.clearChangeFlags();
|
||||
}
|
||||
|
||||
private void updateTtl(HazelcastSession session) {
|
||||
this.sessions.setTtl(session.getId(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HazelcastSession findById(String id) {
|
||||
MapSession saved = this.sessions.get(id);
|
||||
if (saved == null) {
|
||||
return null;
|
||||
}
|
||||
if (saved.isExpired()) {
|
||||
deleteById(saved.getId());
|
||||
return null;
|
||||
}
|
||||
return new HazelcastSession(saved, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
this.sessions.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, HazelcastSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
|
||||
Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size());
|
||||
for (MapSession session : sessions) {
|
||||
sessionMap.put(session.getId(), new HazelcastSession(session, false));
|
||||
}
|
||||
return sessionMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryAdded(EntryEvent<String, MapSession> event) {
|
||||
MapSession session = event.getValue();
|
||||
if (session.getId().equals(session.getOriginalId())) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session created with id: " + session.getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryEvicted(EntryEvent<String, MapSession> event) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session expired with id: " + event.getOldValue().getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryRemoved(EntryEvent<String, MapSession> event) {
|
||||
MapSession session = event.getOldValue();
|
||||
if (session != null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Session deleted with id: " + session.getId());
|
||||
}
|
||||
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
|
||||
* basis for its mapping. It keeps track if changes have been made since last save.
|
||||
*
|
||||
* @author Aleksandar Stojsavljevic
|
||||
*/
|
||||
final class HazelcastSession implements Session {
|
||||
|
||||
private final MapSession delegate;
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private boolean sessionIdChanged;
|
||||
|
||||
private boolean lastAccessedTimeChanged;
|
||||
|
||||
private boolean maxInactiveIntervalChanged;
|
||||
|
||||
private String originalId;
|
||||
|
||||
private Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
HazelcastSession(MapSession cached, boolean isNew) {
|
||||
this.delegate = cached;
|
||||
this.isNew = isNew;
|
||||
this.originalId = cached.getId();
|
||||
if (this.isNew || (HazelcastSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames()
|
||||
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.delegate.setLastAccessedTime(lastAccessedTime);
|
||||
this.lastAccessedTimeChanged = true;
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.delegate.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.delegate.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.delegate.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
String newSessionId = this.delegate.changeSessionId();
|
||||
this.sessionIdChanged = true;
|
||||
return newSessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.delegate.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.delegate.setMaxInactiveInterval(interval);
|
||||
this.maxInactiveIntervalChanged = true;
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.delegate.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
T attributeValue = this.delegate.getAttribute(attributeName);
|
||||
if (attributeValue != null && HazelcastSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(attributeName, attributeValue);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.delegate.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.delegate.setAttribute(attributeName, attributeValue);
|
||||
this.delta.put(attributeName, attributeValue);
|
||||
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
|
||||
Map<String, String> indexes = HazelcastSessionRepository.this.indexResolver.resolveIndexesFor(this);
|
||||
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
|
||||
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
|
||||
}
|
||||
flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
setAttribute(attributeName, null);
|
||||
}
|
||||
|
||||
MapSession getDelegate() {
|
||||
return this.delegate;
|
||||
}
|
||||
|
||||
boolean hasChanges() {
|
||||
return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
|
||||
}
|
||||
|
||||
void clearChangeFlags() {
|
||||
this.isNew = false;
|
||||
this.lastAccessedTimeChanged = false;
|
||||
this.sessionIdChanged = false;
|
||||
this.maxInactiveIntervalChanged = false;
|
||||
this.delta.clear();
|
||||
}
|
||||
|
||||
private void flushImmediateIfNecessary() {
|
||||
if (HazelcastSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
HazelcastSessionRepository.this.save(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ import com.hazelcast.map.AbstractEntryProcessor;
|
||||
import com.hazelcast.map.EntryProcessor;
|
||||
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
|
||||
|
||||
/**
|
||||
* Hazelcast {@link EntryProcessor} responsible for handling updates to session.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 1.3.4
|
||||
* @see HazelcastSessionRepository#save(HazelcastSessionRepository.HazelcastSession)
|
||||
* @see HazelcastIndexedSessionRepository#save(HazelcastSession)
|
||||
*/
|
||||
public class SessionUpdateEntryProcessor extends AbstractEntryProcessor<String, MapSession> implements Offloadable {
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ import java.lang.annotation.Target;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||
|
||||
/**
|
||||
* Qualifier annotation for a {@link HazelcastInstance} to be injected in
|
||||
* {@link HazelcastSessionRepository}.
|
||||
* {@link HazelcastIndexedSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.0.0
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.hazelcast.HazelcastFlushMode;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(HazelcastHttpSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableHazelcastHttpSession {
|
||||
|
||||
/**
|
||||
@@ -81,10 +81,11 @@ public @interface EnableHazelcastHttpSession {
|
||||
|
||||
/**
|
||||
* This is the name of the Map that will be used in Hazelcast to store the session
|
||||
* data. Default is {@link HazelcastSessionRepository#DEFAULT_SESSION_MAP_NAME}.
|
||||
* data. Default is
|
||||
* {@link HazelcastIndexedSessionRepository#DEFAULT_SESSION_MAP_NAME}.
|
||||
* @return the name of the Map to store the sessions in Hazelcast
|
||||
*/
|
||||
String sessionMapName() default HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME;
|
||||
String sessionMapName() default HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
|
||||
|
||||
/**
|
||||
* Flush mode for the Hazelcast sessions. The default is {@code ON_SAVE} which only
|
||||
|
||||
@@ -31,12 +31,14 @@ import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
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.hazelcast.HazelcastFlushMode;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -56,7 +58,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
|
||||
|
||||
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
|
||||
|
||||
private String sessionMapName = HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME;
|
||||
private String sessionMapName = HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
@@ -66,12 +68,18 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
private List<SessionRepositoryCustomizer<HazelcastSessionRepository>> sessionRepositoryCustomizers;
|
||||
private IndexResolver<Session> indexResolver;
|
||||
|
||||
private List<SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>> sessionRepositoryCustomizers;
|
||||
|
||||
@Bean
|
||||
public HazelcastSessionRepository sessionRepository() {
|
||||
HazelcastSessionRepository sessionRepository = new HazelcastSessionRepository(this.hazelcastInstance);
|
||||
public HazelcastIndexedSessionRepository sessionRepository() {
|
||||
HazelcastIndexedSessionRepository sessionRepository = new HazelcastIndexedSessionRepository(
|
||||
this.hazelcastInstance);
|
||||
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
|
||||
if (this.indexResolver != null) {
|
||||
sessionRepository.setIndexResolver(this.indexResolver);
|
||||
}
|
||||
if (StringUtils.hasText(this.sessionMapName)) {
|
||||
sessionRepository.setSessionMapName(this.sessionMapName);
|
||||
}
|
||||
@@ -120,9 +128,14 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setIndexResolver(IndexResolver<Session> indexResolver) {
|
||||
this.indexResolver = indexResolver;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<SessionRepositoryCustomizer<HazelcastSessionRepository>> sessionRepositoryCustomizers) {
|
||||
ObjectProvider<SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -57,12 +57,12 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link HazelcastSessionRepository}.
|
||||
* Tests for {@link HazelcastIndexedSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Aleksandar Stojsavljevic
|
||||
*/
|
||||
class HazelcastSessionRepositoryTests {
|
||||
class HazelcastIndexedSessionRepositoryTests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
@@ -71,18 +71,18 @@ class HazelcastSessionRepositoryTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
private IMap<String, MapSession> sessions = mock(IMap.class);
|
||||
|
||||
private HazelcastSessionRepository repository;
|
||||
private HazelcastIndexedSessionRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
given(this.hazelcastInstance.<String, MapSession>getMap(anyString())).willReturn(this.sessions);
|
||||
this.repository = new HazelcastSessionRepository(this.hazelcastInstance);
|
||||
this.repository = new HazelcastIndexedSessionRepository(this.hazelcastInstance);
|
||||
this.repository.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorNullHazelcastInstance() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new HazelcastSessionRepository(null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new HazelcastIndexedSessionRepository(null))
|
||||
.withMessage("HazelcastInstance must not be null");
|
||||
}
|
||||
|
||||
@@ -28,10 +28,12 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.annotation.Order;
|
||||
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.hazelcast.HazelcastFlushMode;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository;
|
||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@@ -47,7 +49,6 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Vedran Pavic
|
||||
* @author Aleksandar Stojsavljevic
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
private static final String MAP_NAME = "spring:test:sessions";
|
||||
@@ -74,14 +75,14 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void defaultConfiguration() {
|
||||
registerAndRefresh(DefaultConfiguration.class);
|
||||
|
||||
assertThat(this.context.getBean(HazelcastSessionRepository.class)).isNotNull();
|
||||
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customTableName() {
|
||||
registerAndRefresh(CustomSessionMapNameConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastHttpSessionConfiguration configuration = this.context.getBean(HazelcastHttpSessionConfiguration.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName")).isEqualTo(MAP_NAME);
|
||||
@@ -91,7 +92,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void setCustomSessionMapName() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomSessionMapNameSetConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastHttpSessionConfiguration configuration = this.context.getBean(HazelcastHttpSessionConfiguration.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName")).isEqualTo(MAP_NAME);
|
||||
@@ -101,7 +102,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void setCustomMaxInactiveIntervalInSeconds() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomMaxInactiveIntervalInSecondsSetConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
|
||||
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
@@ -111,7 +112,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void customMaxInactiveIntervalInSeconds() {
|
||||
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
|
||||
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
@@ -121,7 +122,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void customFlushImmediately() {
|
||||
registerAndRefresh(CustomFlushImmediatelyConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void customFlushImmediatelyLegacy() {
|
||||
registerAndRefresh(CustomFlushImmediatelyLegacyConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -139,7 +140,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void setCustomFlushImmediately() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomFlushImmediatelySetConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -148,7 +149,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void setCustomFlushImmediatelyLegacy() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomFlushImmediatelySetLegacyConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
|
||||
}
|
||||
@@ -156,22 +157,22 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
@Test
|
||||
void customSaveModeAnnotation() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionAnnotationConfiguration.class);
|
||||
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class))
|
||||
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSaveModeSetter() {
|
||||
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionSetterConfiguration.class);
|
||||
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
|
||||
SaveMode.ALWAYS);
|
||||
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class))
|
||||
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void qualifiedHazelcastInstanceConfiguration() {
|
||||
registerAndRefresh(QualifiedHazelcastInstanceConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastInstance hazelcastInstance = this.context.getBean("qualifiedHazelcastInstance",
|
||||
HazelcastInstance.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -184,7 +185,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void primaryHazelcastInstanceConfiguration() {
|
||||
registerAndRefresh(PrimaryHazelcastInstanceConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastInstance hazelcastInstance = this.context.getBean("primaryHazelcastInstance", HazelcastInstance.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(hazelcastInstance).isNotNull();
|
||||
@@ -196,7 +197,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void qualifiedAndPrimaryHazelcastInstanceConfiguration() {
|
||||
registerAndRefresh(QualifiedAndPrimaryHazelcastInstanceConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastInstance hazelcastInstance = this.context.getBean("qualifiedHazelcastInstance",
|
||||
HazelcastInstance.class);
|
||||
assertThat(repository).isNotNull();
|
||||
@@ -209,7 +210,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
void namedHazelcastInstanceConfiguration() {
|
||||
registerAndRefresh(NamedHazelcastInstanceConfiguration.class);
|
||||
|
||||
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
HazelcastInstance hazelcastInstance = this.context.getBean("hazelcastInstance", HazelcastInstance.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(hazelcastInstance).isNotNull();
|
||||
@@ -224,10 +225,22 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
.withMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customIndexResolverConfiguration() {
|
||||
registerAndRefresh(CustomIndexResolverConfiguration.class);
|
||||
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(indexResolver).isNotNull();
|
||||
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionRepositoryCustomizer() {
|
||||
registerAndRefresh(SessionRepositoryCustomizerConfiguration.class);
|
||||
HazelcastSessionRepository sessionRepository = this.context.getBean(HazelcastSessionRepository.class);
|
||||
HazelcastIndexedSessionRepository sessionRepository = this.context
|
||||
.getBean(HazelcastIndexedSessionRepository.class);
|
||||
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
|
||||
MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
@@ -249,7 +262,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
static IMap<Object, Object> defaultHazelcastInstanceSessions = mock(IMap.class);
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance defaultHazelcastInstance() {
|
||||
HazelcastInstance defaultHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(defaultHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -303,6 +316,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelySetLegacyConfiguration extends HazelcastHttpSessionConfiguration {
|
||||
|
||||
CustomFlushImmediatelySetLegacyConfiguration() {
|
||||
@@ -333,6 +347,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Configuration
|
||||
@EnableHazelcastHttpSession(hazelcastFlushMode = HazelcastFlushMode.IMMEDIATE)
|
||||
@SuppressWarnings("deprecation")
|
||||
static class CustomFlushImmediatelyLegacyConfiguration extends BaseConfiguration {
|
||||
|
||||
}
|
||||
@@ -346,7 +361,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionHazelcastInstance
|
||||
public HazelcastInstance qualifiedHazelcastInstance() {
|
||||
HazelcastInstance qualifiedHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(qualifiedHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -363,7 +378,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public HazelcastInstance primaryHazelcastInstance() {
|
||||
HazelcastInstance primaryHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(primaryHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -383,7 +398,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@SpringSessionHazelcastInstance
|
||||
public HazelcastInstance qualifiedHazelcastInstance() {
|
||||
HazelcastInstance qualifiedHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(qualifiedHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -391,7 +406,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public HazelcastInstance primaryHazelcastInstance() {
|
||||
HazelcastInstance primaryHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(primaryHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -407,7 +422,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
static IMap<Object, Object> hazelcastInstanceSessions = mock(IMap.class);
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance hazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(hazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -423,7 +438,7 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
static IMap<Object, Object> secondaryHazelcastInstanceSessions = mock(IMap.class);
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance secondaryHazelcastInstance() {
|
||||
HazelcastInstance secondaryHazelcastInstance() {
|
||||
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
|
||||
given(hazelcastInstance.getMap(anyString())).willReturn(secondaryHazelcastInstanceSessions);
|
||||
return hazelcastInstance;
|
||||
@@ -431,18 +446,29 @@ class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableHazelcastHttpSession
|
||||
static class CustomIndexResolverConfiguration extends BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
IndexResolver<Session> indexResolver() {
|
||||
return mock(IndexResolver.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableHazelcastHttpSession
|
||||
static class SessionRepositoryCustomizerConfiguration extends BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public SessionRepositoryCustomizer<HazelcastSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> sessionRepositoryCustomizerOne() {
|
||||
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SessionRepositoryCustomizer<HazelcastSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
|
||||
return (sessionRepository) -> sessionRepository
|
||||
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies {
|
||||
integrationTestCompile "com.h2database:h2"
|
||||
integrationTestCompile "com.ibm.db2:jcc"
|
||||
integrationTestCompile "com.microsoft.sqlserver:mssql-jdbc"
|
||||
integrationTestCompile "com.oracle.ojdbc:ojdbc8"
|
||||
integrationTestCompile "com.zaxxer:HikariCP"
|
||||
integrationTestCompile "mysql:mysql-connector-java"
|
||||
integrationTestCompile "org.apache.derby:derby"
|
||||
@@ -29,3 +30,7 @@ dependencies {
|
||||
integrationTestCompile "org.testcontainers:oracle-xe"
|
||||
integrationTestCompile "org.testcontainers:postgresql"
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
maxParallelForks = 4
|
||||
}
|
||||
|
||||
@@ -26,18 +26,17 @@ import org.springframework.jdbc.datasource.init.DataSourceInitializer;
|
||||
import org.springframework.jdbc.datasource.init.DatabasePopulator;
|
||||
|
||||
/**
|
||||
* Abstract base class for Testcontainers based {@link JdbcOperationsSessionRepository}
|
||||
* integration tests.
|
||||
* Base class for Testcontainers based {@link JdbcIndexedSessionRepository} integration
|
||||
* tests.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
abstract class AbstractContainerJdbcOperationsSessionRepositoryITests
|
||||
extends AbstractJdbcOperationsSessionRepositoryITests {
|
||||
abstract class AbstractContainerJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
static class BaseContainerConfig extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public HikariDataSource dataSource(JdbcDatabaseContainer databaseContainer) {
|
||||
HikariDataSource dataSource(JdbcDatabaseContainer databaseContainer) {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(databaseContainer.getJdbcUrl());
|
||||
dataSource.setUsername(databaseContainer.getUsername());
|
||||
@@ -46,7 +45,7 @@ abstract class AbstractContainerJdbcOperationsSessionRepositoryITests
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator databasePopulator) {
|
||||
DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator databasePopulator) {
|
||||
DataSourceInitializer initializer = new DataSourceInitializer();
|
||||
initializer.setDataSource(dataSource);
|
||||
initializer.setDatabasePopulator(databasePopulator);
|
||||
@@ -38,6 +38,7 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.jdbc.JdbcIndexedSessionRepository.JdbcSession;
|
||||
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
@@ -46,18 +47,18 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link JdbcOperationsSessionRepository} integration tests.
|
||||
* Base class for {@link JdbcIndexedSessionRepository} integration tests.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
abstract class AbstractJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
|
||||
|
||||
@Autowired
|
||||
private JdbcOperationsSessionRepository repository;
|
||||
private JdbcIndexedSessionRepository repository;
|
||||
|
||||
private SecurityContext context;
|
||||
|
||||
@@ -76,10 +77,10 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void saveWhenNoAttributesThenCanBeFound() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(toSave);
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
|
||||
JdbcSession session = this.repository.findById(toSave.getId());
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.isChanged()).isFalse();
|
||||
@@ -90,7 +91,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
void saves() {
|
||||
String username = "saves-" + System.currentTimeMillis();
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
String expectedAttributeName = "a";
|
||||
String expectedAttributeValue = "b";
|
||||
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
|
||||
@@ -103,7 +104,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
|
||||
JdbcSession session = this.repository.findById(toSave.getId());
|
||||
|
||||
assertThat(session.getId()).isEqualTo(toSave.getId());
|
||||
assertThat(session.isChanged()).isFalse();
|
||||
@@ -120,14 +121,14 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
@Transactional(readOnly = true)
|
||||
void savesInReadOnlyTransaction() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(toSave);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putAllOnSingleAttrDoesNotRemoveOld() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute("a", "b");
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -138,7 +139,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
this.repository.save(toSave);
|
||||
toSave = this.repository.findById(toSave.getId());
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
|
||||
JdbcSession session = this.repository.findById(toSave.getId());
|
||||
assertThat(session.isChanged()).isFalse();
|
||||
assertThat(session.getDelta()).isEmpty();
|
||||
assertThat(session.getAttributeNames().size()).isEqualTo(2);
|
||||
@@ -150,7 +151,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void updateLastAccessedTime() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -159,7 +160,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setLastAccessedTime(lastAccessedTime);
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
|
||||
JdbcSession session = this.repository.findById(toSave.getId());
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.isChanged()).isFalse();
|
||||
@@ -172,13 +173,13 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByPrincipalName() {
|
||||
String principalName = "findByPrincipalName" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -194,15 +195,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByPrincipalNameExpireRemovesIndex() {
|
||||
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
|
||||
|
||||
this.repository.save(toSave);
|
||||
this.repository.cleanUpExpiredSessions();
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(0);
|
||||
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
|
||||
@@ -211,7 +212,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByPrincipalNameNoPrincipalNameChange() {
|
||||
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -219,8 +220,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -233,7 +234,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByPrincipalNameNoPrincipalNameChangeReload() {
|
||||
String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -243,8 +244,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -257,7 +258,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByDeletedPrincipalName() {
|
||||
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -265,8 +266,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute(INDEX_NAME, null);
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
@@ -275,7 +276,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
void findByChangedPrincipalName() {
|
||||
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -283,8 +284,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute(INDEX_NAME, principalNameChanged);
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
|
||||
@@ -300,17 +301,17 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
@Test
|
||||
void findByDeletedPrincipalNameReload() {
|
||||
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
getSession.setAttribute(INDEX_NAME, null);
|
||||
this.repository.save(getSession);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
@@ -319,18 +320,18 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
void findByChangedPrincipalNameReload() {
|
||||
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(INDEX_NAME, principalName);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
|
||||
getSession.setAttribute(INDEX_NAME, principalNameChanged);
|
||||
this.repository.save(getSession);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
principalName);
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
|
||||
@@ -345,13 +346,13 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findBySecurityPrincipalName() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -366,15 +367,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findBySecurityPrincipalNameExpireRemovesIndex() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
|
||||
|
||||
this.repository.save(toSave);
|
||||
this.repository.cleanUpExpiredSessions();
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(0);
|
||||
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
|
||||
@@ -382,7 +383,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameNoSecurityPrincipalNameChange() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -390,8 +391,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -403,7 +404,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findByPrincipalNameNoSecurityPrincipalNameChangeReload() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -413,8 +414,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute("other", "value");
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).hasSize(1);
|
||||
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
|
||||
@@ -426,7 +427,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findByDeletedSecurityPrincipalName() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -434,15 +435,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, null);
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByChangedSecurityPrincipalName() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
@@ -450,8 +451,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(toSave);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
|
||||
@@ -466,35 +467,35 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void findByDeletedSecurityPrincipalNameReload() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
getSession.setAttribute(INDEX_NAME, null);
|
||||
this.repository.save(getSession);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getChangedSecurityName());
|
||||
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByChangedSecurityPrincipalNameReload() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
JdbcSession getSession = this.repository.findById(toSave.getId());
|
||||
|
||||
getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
|
||||
this.repository.save(getSession);
|
||||
|
||||
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
|
||||
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
|
||||
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
|
||||
getSecurityName());
|
||||
assertThat(findByPrincipalName).isEmpty();
|
||||
|
||||
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
|
||||
@@ -509,7 +510,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void cleanupInactiveSessionsUsingRepositoryDefinedInterval() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
|
||||
this.repository.save(session);
|
||||
|
||||
@@ -537,7 +538,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
// gh-580
|
||||
@Test
|
||||
void cleanupInactiveSessionsUsingSessionDefinedInterval() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
session.setMaxInactiveInterval(Duration.ofMinutes(45));
|
||||
|
||||
this.repository.save(session);
|
||||
@@ -567,12 +568,12 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
void changeSessionIdWhenOnlyChangeId() {
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
toSave.setAttribute(attrName, attrValue);
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession findById = this.repository.findById(toSave.getId());
|
||||
JdbcSession findById = this.repository.findById(toSave.getId());
|
||||
|
||||
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
|
||||
|
||||
@@ -583,7 +584,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
assertThat(this.repository.findById(originalFindById)).isNull();
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
|
||||
JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
|
||||
|
||||
assertThat(findByChangeSessionId.isChanged()).isFalse();
|
||||
assertThat(findByChangeSessionId.getDelta()).isEmpty();
|
||||
@@ -592,7 +593,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenChangeTwice() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
@@ -612,11 +613,11 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
String attrName = "changeSessionId";
|
||||
String attrValue = "changeSessionId-value";
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
|
||||
this.repository.save(toSave);
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession findById = this.repository.findById(toSave.getId());
|
||||
JdbcSession findById = this.repository.findById(toSave.getId());
|
||||
|
||||
findById.setAttribute(attrName, attrValue);
|
||||
|
||||
@@ -627,7 +628,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
assertThat(this.repository.findById(originalFindById)).isNull();
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
|
||||
JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
|
||||
|
||||
assertThat(findByChangeSessionId.isChanged()).isFalse();
|
||||
assertThat(findByChangeSessionId.getDelta()).isEmpty();
|
||||
@@ -636,7 +637,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test
|
||||
void changeSessionIdWhenHasNotSaved() {
|
||||
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
|
||||
JdbcSession toSave = this.repository.createSession();
|
||||
String originalId = toSave.getId();
|
||||
toSave.changeSessionId();
|
||||
|
||||
@@ -648,7 +649,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1070
|
||||
void saveUpdatedAddAndModifyAttribute() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
session.setAttribute("testName", "testValue1");
|
||||
@@ -661,7 +662,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1070
|
||||
void saveUpdatedAddAndRemoveAttribute() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
session.setAttribute("testName", "testValue");
|
||||
@@ -674,7 +675,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1070
|
||||
void saveUpdatedModifyAndRemoveAttribute() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
session.setAttribute("testName", "testValue1");
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
@@ -688,7 +689,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1070
|
||||
void saveUpdatedRemoveAndAddAttribute() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
session.setAttribute("testName", "testValue1");
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
@@ -702,7 +703,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1031
|
||||
void saveDeleted() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
this.repository.deleteById(session.getId());
|
||||
@@ -714,7 +715,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1031
|
||||
void saveDeletedAddAttribute() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
this.repository.deleteById(session.getId());
|
||||
@@ -727,7 +728,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
@Test // gh-1133
|
||||
void sessionFromStoreResolvesAttributesLazily() {
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
session.setAttribute("attribute1", "value1");
|
||||
session.setAttribute("attribute2", "value2");
|
||||
this.repository.save(session);
|
||||
@@ -749,7 +750,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
String attributeName = "largeAttribute";
|
||||
int arraySize = 4000;
|
||||
|
||||
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
|
||||
JdbcSession session = this.repository.createSession();
|
||||
session.setAttribute(attributeName, new byte[arraySize]);
|
||||
this.repository.save(session);
|
||||
session = this.repository.findById(session.getId());
|
||||
@@ -770,7 +771,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
|
||||
static class BaseConfig {
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(DataSource dataSource) {
|
||||
PlatformTransactionManager transactionManager(DataSource dataSource) {
|
||||
return new DataSourceTransactionManager(dataSource);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.session.jdbc;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.testcontainers.containers.Db2Container;
|
||||
import org.testcontainers.containers.JdbcDatabaseContainer;
|
||||
import org.testcontainers.containers.MSSQLServerContainer;
|
||||
@@ -23,6 +26,7 @@ import org.testcontainers.containers.MariaDBContainer;
|
||||
import org.testcontainers.containers.MySQLContainer;
|
||||
import org.testcontainers.containers.OracleContainer;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
|
||||
|
||||
/**
|
||||
* Factories for various {@link JdbcDatabaseContainer}s.
|
||||
@@ -54,8 +58,8 @@ final class DatabaseContainers {
|
||||
return new MySql8Container();
|
||||
}
|
||||
|
||||
static OracleContainer oracle() {
|
||||
return new OracleContainer();
|
||||
static OracleXeContainer oracleXe() {
|
||||
return new OracleXeContainer();
|
||||
}
|
||||
|
||||
static PostgreSQLContainer postgreSql9() {
|
||||
@@ -100,7 +104,7 @@ final class DatabaseContainers {
|
||||
private static class MariaDb10Container extends MariaDBContainer<MariaDb10Container> {
|
||||
|
||||
MariaDb10Container() {
|
||||
super("mariadb:10.4.7");
|
||||
super("mariadb:10.4.18");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,6 +153,24 @@ final class DatabaseContainers {
|
||||
|
||||
}
|
||||
|
||||
private static class OracleXeContainer extends OracleContainer {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
super.configure();
|
||||
this.waitStrategy = new LogMessageWaitStrategy().withRegEx(".*DATABASE IS READY TO USE!.*\\s")
|
||||
.withStartupTimeout(Duration.of(10, ChronoUnit.MINUTES));
|
||||
setShmSize(1024L * 1024L * 1024L);
|
||||
addEnv("ORACLE_PWD", getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void waitUntilContainerStarted() {
|
||||
getWaitStrategy().waitUntilReady(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class PostgreSql9Container extends PostgreSQLContainer<PostgreSql9Container> {
|
||||
|
||||
PostgreSql9Container() {
|
||||
@@ -176,7 +198,7 @@ final class DatabaseContainers {
|
||||
private static class SqlServer2017Container extends MSSQLServerContainer<SqlServer2017Container> {
|
||||
|
||||
SqlServer2017Container() {
|
||||
super("mcr.microsoft.com/mssql/server:2017-cu16");
|
||||
super("mcr.microsoft.com/mssql/server:2017-CU16");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using IBM DB2 11.x
|
||||
* database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using IBM DB2 11.x database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class Db211JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class Db211JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public Db2Container databaseContainer() {
|
||||
Db2Container databaseContainer() {
|
||||
Db2Container databaseContainer = DatabaseContainers.db211();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.db2();
|
||||
}
|
||||
|
||||
@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using Derby database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using Derby database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class DerbyJdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
|
||||
class DerbyJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public EmbeddedDatabase dataSource() {
|
||||
EmbeddedDatabase dataSource() {
|
||||
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY)
|
||||
.addScript("org/springframework/session/jdbc/schema-derby.sql").build();
|
||||
}
|
||||
@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using H2 database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using H2 database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class H2JdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
|
||||
class H2JdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public EmbeddedDatabase dataSource() {
|
||||
EmbeddedDatabase dataSource() {
|
||||
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
|
||||
.addScript("org/springframework/session/jdbc/schema-h2.sql").build();
|
||||
}
|
||||
@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using HSQLDB database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using HSQLDB database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class HsqldbJdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
|
||||
class HsqldbJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public EmbeddedDatabase dataSource() {
|
||||
EmbeddedDatabase dataSource() {
|
||||
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
|
||||
.addScript("org/springframework/session/jdbc/schema-hsqldb.sql").build();
|
||||
}
|
||||
@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using MariaDB 10.x
|
||||
* database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 10.x database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class MariaDb10JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class MariaDb10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public MariaDBContainer databaseContainer() {
|
||||
MariaDBContainer databaseContainer() {
|
||||
MariaDBContainer databaseContainer = DatabaseContainers.mariaDb10();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.mySql();
|
||||
}
|
||||
|
||||
@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using MariaDB 5.x
|
||||
* database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 5.x database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class MariaDb5JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class MariaDb5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public MariaDBContainer databaseContainer() {
|
||||
MariaDBContainer databaseContainer() {
|
||||
MariaDBContainer databaseContainer = DatabaseContainers.mariaDb5();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.mySql();
|
||||
}
|
||||
|
||||
@@ -27,27 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using MySQL 5.x database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 5.x database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class MySql5JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class MySql5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public MySQLContainer databaseContainer() {
|
||||
MySQLContainer databaseContainer() {
|
||||
MySQLContainer databaseContainer = DatabaseContainers.mySql5();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.mySql();
|
||||
}
|
||||
|
||||
@@ -27,27 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using MySQL 8.x database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 8.x database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class MySql8JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class MySql8JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public MySQLContainer databaseContainer() {
|
||||
MySQLContainer databaseContainer() {
|
||||
MySQLContainer databaseContainer = DatabaseContainers.mySql8();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.mySql();
|
||||
}
|
||||
|
||||
@@ -28,27 +28,24 @@ import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using Oracle database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using Oracle database.
|
||||
* <p>
|
||||
* This test is conditional on presence of Oracle JDBC driver on the classpath and
|
||||
* Testcontainers property {@code oracle.container.image} being set.
|
||||
* This test is conditional on Testcontainers property {@code oracle.container.image}
|
||||
* being set.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class OracleJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class OracleJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() {
|
||||
Assumptions.assumeTrue(ClassUtils.isPresent("oracle.jdbc.OracleDriver", null),
|
||||
"Oracle JDBC driver is present on the classpath");
|
||||
Assumptions.assumeTrue(
|
||||
TestcontainersConfiguration.getInstance().getProperties().getProperty("oracle.container.image") != null,
|
||||
TestcontainersConfiguration.getInstance().getProperties().containsKey("oracle.container.image"),
|
||||
"Testcontainers property `oracle.container.image` is set");
|
||||
}
|
||||
|
||||
@@ -56,14 +53,14 @@ class OracleJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcO
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public OracleContainer databaseContainer() {
|
||||
OracleContainer databaseContainer = DatabaseContainers.oracle();
|
||||
OracleContainer databaseContainer() {
|
||||
OracleContainer databaseContainer = DatabaseContainers.oracleXe();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.oracle();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 10.x
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 10.x
|
||||
* database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class PostgreSql10JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class PostgreSql10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql10();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.postgreSql();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 11.x
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 11.x
|
||||
* database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class PostgreSql11JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class PostgreSql11JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql11();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.postgreSql();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 9.x
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 9.x
|
||||
* database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class PostgreSql9JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class PostgreSql9JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer() {
|
||||
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql9();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.postgreSql();
|
||||
}
|
||||
|
||||
@@ -27,28 +27,28 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link JdbcOperationsSessionRepository} using Microsoft SQL
|
||||
* Server 2017 database.
|
||||
* Integration tests for {@link JdbcIndexedSessionRepository} using Microsoft SQL Server
|
||||
* 2017 database.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
class SqlServerJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
|
||||
class SqlServerJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
|
||||
|
||||
@Configuration
|
||||
static class Config extends BaseContainerConfig {
|
||||
|
||||
@Bean
|
||||
public MSSQLServerContainer databaseContainer() {
|
||||
MSSQLServerContainer databaseContainer() {
|
||||
MSSQLServerContainer databaseContainer = DatabaseContainers.sqlServer2017();
|
||||
databaseContainer.start();
|
||||
return databaseContainer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceDatabasePopulator databasePopulator() {
|
||||
ResourceDatabasePopulator databasePopulator() {
|
||||
return DatabasePopulators.sqlServer();
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ibmcom/db2:11.5.0.0a
|
||||
mcr.microsoft.com/mssql/server:2017-cu16
|
||||
mcr.microsoft.com/mssql/server:2017-CU16
|
||||
|
||||
@@ -0,0 +1,873 @@
|
||||
/*
|
||||
* 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.jdbc;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.core.serializer.support.DeserializingConverter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
import org.springframework.jdbc.support.lob.DefaultLobHandler;
|
||||
import org.springframework.jdbc.support.lob.LobHandler;
|
||||
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.transaction.support.TransactionOperations;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.session.SessionRepository} implementation that uses
|
||||
* Spring's {@link JdbcOperations} to store sessions in a relational database. This
|
||||
* implementation does not support publishing of session events.
|
||||
* <p>
|
||||
* An example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
|
||||
*
|
||||
* // ... configure jdbcTemplate ...
|
||||
*
|
||||
* TransactionTemplate transactionTemplate = new TransactionTemplate();
|
||||
*
|
||||
* // ... configure transactionTemplate ...
|
||||
*
|
||||
* JdbcIndexedSessionRepository sessionRepository =
|
||||
* new JdbcIndexedSessionRepository(jdbcTemplate, transactionTemplate);
|
||||
* </pre>
|
||||
*
|
||||
* For additional information on how to create and configure {@code JdbcTemplate} and
|
||||
* {@code TransactionTemplate}, refer to the <a href=
|
||||
* "https://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
|
||||
* Spring Framework Reference Documentation</a>.
|
||||
* <p>
|
||||
* By default, this implementation uses <code>SPRING_SESSION</code> and
|
||||
* <code>SPRING_SESSION_ATTRIBUTES</code> tables to store sessions. Note that the table
|
||||
* name can be customized using the {@link #setTableName(String)} method. In that case the
|
||||
* table used to store attributes will be named using the provided table name, suffixed
|
||||
* with <code>_ATTRIBUTES</code>.
|
||||
*
|
||||
* Depending on your database, the table definition can be described as below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* CREATE TABLE SPRING_SESSION (
|
||||
* PRIMARY_ID CHAR(36) NOT NULL,
|
||||
* SESSION_ID CHAR(36) NOT NULL,
|
||||
* CREATION_TIME BIGINT NOT NULL,
|
||||
* LAST_ACCESS_TIME BIGINT NOT NULL,
|
||||
* MAX_INACTIVE_INTERVAL INT NOT NULL,
|
||||
* EXPIRY_TIME BIGINT NOT NULL,
|
||||
* PRINCIPAL_NAME VARCHAR(100),
|
||||
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
|
||||
* );
|
||||
*
|
||||
* CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
|
||||
* CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (EXPIRY_TIME);
|
||||
* CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
|
||||
*
|
||||
* CREATE TABLE SPRING_SESSION_ATTRIBUTES (
|
||||
* SESSION_PRIMARY_ID CHAR(36) NOT NULL,
|
||||
* ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
|
||||
* ATTRIBUTE_BYTES BYTEA NOT NULL,
|
||||
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
|
||||
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
|
||||
* );
|
||||
*
|
||||
* CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_PRIMARY_ID);
|
||||
* </pre>
|
||||
*
|
||||
* Due to the differences between the various database vendors, especially when it comes
|
||||
* to storing binary data, make sure to use SQL script specific to your database. Scripts
|
||||
* for most major database vendors are packaged as
|
||||
* <code>org/springframework/session/jdbc/schema-*.sql</code>, where <code>*</code> is the
|
||||
* target database type.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Craig Andrews
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class JdbcIndexedSessionRepository
|
||||
implements FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession> {
|
||||
|
||||
/**
|
||||
* The default name of database table used by Spring Session to store sessions.
|
||||
*/
|
||||
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
// @formatter:off
|
||||
private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
|
||||
+ "SELECT PRIMARY_ID, ?, ? "
|
||||
+ "FROM %TABLE_NAME% "
|
||||
+ "WHERE SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String GET_SESSION_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
|
||||
+ "FROM %TABLE_NAME% S "
|
||||
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
|
||||
+ "WHERE S.SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? "
|
||||
+ "WHERE PRIMARY_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? "
|
||||
+ "WHERE SESSION_PRIMARY_ID = ? "
|
||||
+ "AND ATTRIBUTE_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "DELETE FROM %TABLE_NAME%_ATTRIBUTES "
|
||||
+ "WHERE SESSION_PRIMARY_ID = ? "
|
||||
+ "AND ATTRIBUTE_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% "
|
||||
+ "WHERE SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
|
||||
+ "FROM %TABLE_NAME% S "
|
||||
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
|
||||
+ "WHERE S.PRINCIPAL_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "DELETE FROM %TABLE_NAME% "
|
||||
+ "WHERE EXPIRY_TIME < ?";
|
||||
// @formatter:on
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JdbcIndexedSessionRepository.class);
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
|
||||
private final TransactionOperations transactionOperations;
|
||||
|
||||
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
|
||||
|
||||
/**
|
||||
* The name of database table used by Spring Session to store sessions.
|
||||
*/
|
||||
private String tableName = DEFAULT_TABLE_NAME;
|
||||
|
||||
private String createSessionQuery;
|
||||
|
||||
private String createSessionAttributeQuery;
|
||||
|
||||
private String getSessionQuery;
|
||||
|
||||
private String updateSessionQuery;
|
||||
|
||||
private String updateSessionAttributeQuery;
|
||||
|
||||
private String deleteSessionAttributeQuery;
|
||||
|
||||
private String deleteSessionQuery;
|
||||
|
||||
private String listSessionsByPrincipalNameQuery;
|
||||
|
||||
private String deleteSessionsByExpiryTimeQuery;
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link JdbcSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
|
||||
private ConversionService conversionService = createDefaultConversionService();
|
||||
|
||||
private LobHandler lobHandler = new DefaultLobHandler();
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
|
||||
* {@link JdbcOperations} and {@link TransactionOperations} to manage sessions.
|
||||
* @param jdbcOperations the {@link JdbcOperations} to use
|
||||
* @param transactionOperations the {@link TransactionOperations} to use
|
||||
*/
|
||||
public JdbcIndexedSessionRepository(JdbcOperations jdbcOperations, TransactionOperations transactionOperations) {
|
||||
Assert.notNull(jdbcOperations, "jdbcOperations must not be null");
|
||||
Assert.notNull(transactionOperations, "transactionOperations must not be null");
|
||||
this.jdbcOperations = jdbcOperations;
|
||||
this.transactionOperations = transactionOperations;
|
||||
prepareQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of database table used to store sessions.
|
||||
* @param tableName the database table name
|
||||
*/
|
||||
public void setTableName(String tableName) {
|
||||
Assert.hasText(tableName, "Table name must not be empty");
|
||||
this.tableName = tableName.trim();
|
||||
prepareQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to create the session.
|
||||
* @param createSessionQuery the SQL query string
|
||||
*/
|
||||
public void setCreateSessionQuery(String createSessionQuery) {
|
||||
Assert.hasText(createSessionQuery, "Query must not be empty");
|
||||
this.createSessionQuery = createSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to create the session attribute.
|
||||
* @param createSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
|
||||
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
|
||||
this.createSessionAttributeQuery = createSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to retrieve the session.
|
||||
* @param getSessionQuery the SQL query string
|
||||
*/
|
||||
public void setGetSessionQuery(String getSessionQuery) {
|
||||
Assert.hasText(getSessionQuery, "Query must not be empty");
|
||||
this.getSessionQuery = getSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to update the session.
|
||||
* @param updateSessionQuery the SQL query string
|
||||
*/
|
||||
public void setUpdateSessionQuery(String updateSessionQuery) {
|
||||
Assert.hasText(updateSessionQuery, "Query must not be empty");
|
||||
this.updateSessionQuery = updateSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to update the session attribute.
|
||||
* @param updateSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
|
||||
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
|
||||
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the session attribute.
|
||||
* @param deleteSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
|
||||
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
|
||||
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the session.
|
||||
* @param deleteSessionQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionQuery(String deleteSessionQuery) {
|
||||
Assert.hasText(deleteSessionQuery, "Query must not be empty");
|
||||
this.deleteSessionQuery = deleteSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to retrieve the sessions by principal name.
|
||||
* @param listSessionsByPrincipalNameQuery the SQL query string
|
||||
*/
|
||||
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
|
||||
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
|
||||
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the sessions by last access time.
|
||||
* @param deleteSessionsByExpiryTimeQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
|
||||
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
|
||||
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum inactive interval in seconds between requests before newly created
|
||||
* sessions will be invalidated. A negative time indicates that the session will never
|
||||
* timeout. The default is 1800 (30 minutes).
|
||||
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link IndexResolver} to use.
|
||||
* @param indexResolver the index resolver
|
||||
*/
|
||||
public void setIndexResolver(IndexResolver<Session> indexResolver) {
|
||||
Assert.notNull(indexResolver, "indexResolver cannot be null");
|
||||
this.indexResolver = indexResolver;
|
||||
}
|
||||
|
||||
public void setLobHandler(LobHandler lobHandler) {
|
||||
Assert.notNull(lobHandler, "LobHandler must not be null");
|
||||
this.lobHandler = lobHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ConversionService} to use.
|
||||
* @param conversionService the converter to set
|
||||
*/
|
||||
public void setConversionService(ConversionService conversionService) {
|
||||
Assert.notNull(conversionService, "conversionService must not be null");
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flush mode. Default is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the flush mode
|
||||
*/
|
||||
public void setFlushMode(FlushMode flushMode) {
|
||||
Assert.notNull(flushMode, "flushMode must not be null");
|
||||
this.flushMode = flushMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the save mode.
|
||||
* @param saveMode the save mode
|
||||
*/
|
||||
public void setSaveMode(SaveMode saveMode) {
|
||||
Assert.notNull(saveMode, "saveMode must not be null");
|
||||
this.saveMode = saveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdbcSession createSession() {
|
||||
MapSession delegate = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
|
||||
session.flushIfRequired();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(final JdbcSession session) {
|
||||
session.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdbcSession findById(final String id) {
|
||||
final JdbcSession session = this.transactionOperations.execute((status) -> {
|
||||
List<JdbcSession> sessions = JdbcIndexedSessionRepository.this.jdbcOperations.query(
|
||||
JdbcIndexedSessionRepository.this.getSessionQuery, (ps) -> ps.setString(1, id),
|
||||
JdbcIndexedSessionRepository.this.extractor);
|
||||
if (sessions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return sessions.get(0);
|
||||
});
|
||||
|
||||
if (session != null) {
|
||||
if (session.isExpired()) {
|
||||
deleteById(id);
|
||||
}
|
||||
else {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(final String id) {
|
||||
this.transactionOperations.executeWithoutResult((status) -> JdbcIndexedSessionRepository.this.jdbcOperations
|
||||
.update(JdbcIndexedSessionRepository.this.deleteSessionQuery, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, final String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<JdbcSession> sessions = this.transactionOperations
|
||||
.execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations.query(
|
||||
JdbcIndexedSessionRepository.this.listSessionsByPrincipalNameQuery,
|
||||
(ps) -> ps.setString(1, indexValue), JdbcIndexedSessionRepository.this.extractor));
|
||||
|
||||
Map<String, JdbcSession> sessionMap = new HashMap<>(sessions.size());
|
||||
|
||||
for (JdbcSession session : sessions) {
|
||||
sessionMap.put(session.getId(), session);
|
||||
}
|
||||
|
||||
return sessionMap;
|
||||
}
|
||||
|
||||
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
ps.setString(1, attributeName);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
|
||||
serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(3, session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
ps.setString(1, attributeName);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(3, session.getId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
|
||||
serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(2, session.primaryKey);
|
||||
ps.setString(3, attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(2, session.primaryKey);
|
||||
ps.setString(3, attributeName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
ps.setString(1, session.primaryKey);
|
||||
ps.setString(2, attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.deleteSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
ps.setString(1, session.primaryKey);
|
||||
ps.setString(2, attributeName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanUpExpiredSessions() {
|
||||
Integer deletedCount = this.transactionOperations
|
||||
.execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations.update(
|
||||
JdbcIndexedSessionRepository.this.deleteSessionsByExpiryTimeQuery, System.currentTimeMillis()));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Cleaned up " + deletedCount + " expired sessions");
|
||||
}
|
||||
}
|
||||
|
||||
private static GenericConversionService createDefaultConversionService() {
|
||||
GenericConversionService converter = new GenericConversionService();
|
||||
converter.addConverter(Object.class, byte[].class, new SerializingConverter());
|
||||
converter.addConverter(byte[].class, Object.class, new DeserializingConverter());
|
||||
return converter;
|
||||
}
|
||||
|
||||
private String getQuery(String base) {
|
||||
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
|
||||
}
|
||||
|
||||
private void prepareQueries() {
|
||||
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
|
||||
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
|
||||
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
|
||||
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
|
||||
this.listSessionsByPrincipalNameQuery = getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
|
||||
this.deleteSessionsByExpiryTimeQuery = getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
|
||||
}
|
||||
|
||||
private LobHandler getLobHandler() {
|
||||
return this.lobHandler;
|
||||
}
|
||||
|
||||
private byte[] serialize(Object object) {
|
||||
return (byte[]) this.conversionService.convert(object, TypeDescriptor.valueOf(Object.class),
|
||||
TypeDescriptor.valueOf(byte[].class));
|
||||
}
|
||||
|
||||
private Object deserialize(byte[] bytes) {
|
||||
return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
|
||||
TypeDescriptor.valueOf(Object.class));
|
||||
}
|
||||
|
||||
private enum DeltaValue {
|
||||
|
||||
ADDED, UPDATED, REMOVED
|
||||
|
||||
}
|
||||
|
||||
private static <T> Supplier<T> value(T value) {
|
||||
return (value != null) ? () -> value : null;
|
||||
}
|
||||
|
||||
private static <T> Supplier<T> lazily(Supplier<T> supplier) {
|
||||
Supplier<T> lazySupplier = new Supplier<T>() {
|
||||
|
||||
private T value;
|
||||
|
||||
@Override
|
||||
public T get() {
|
||||
if (this.value == null) {
|
||||
this.value = supplier.get();
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (supplier != null) ? lazySupplier : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Session} to use for {@link JdbcIndexedSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
final class JdbcSession implements Session {
|
||||
|
||||
private final Session delegate;
|
||||
|
||||
private final String primaryKey;
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private boolean changed;
|
||||
|
||||
private Map<String, DeltaValue> delta = new HashMap<>();
|
||||
|
||||
JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
|
||||
this.delegate = delegate;
|
||||
this.primaryKey = primaryKey;
|
||||
this.isNew = isNew;
|
||||
if (this.isNew || (JdbcIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(attributeName, DeltaValue.UPDATED));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNew() {
|
||||
return this.isNew;
|
||||
}
|
||||
|
||||
boolean isChanged() {
|
||||
return this.changed;
|
||||
}
|
||||
|
||||
Map<String, DeltaValue> getDelta() {
|
||||
return this.delta;
|
||||
}
|
||||
|
||||
void clearChangeFlags() {
|
||||
this.isNew = false;
|
||||
this.changed = false;
|
||||
this.delta.clear();
|
||||
}
|
||||
|
||||
Instant getExpiryTime() {
|
||||
return getLastAccessedTime().plus(getMaxInactiveInterval());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.delegate.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
this.changed = true;
|
||||
return this.delegate.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
Supplier<T> supplier = this.delegate.getAttribute(attributeName);
|
||||
if (supplier == null) {
|
||||
return null;
|
||||
}
|
||||
T attributeValue = supplier.get();
|
||||
if (attributeValue != null
|
||||
&& JdbcIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(attributeName, DeltaValue.UPDATED);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.delegate.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
|
||||
boolean attributeRemoved = (attributeValue == null);
|
||||
if (!attributeExists && attributeRemoved) {
|
||||
return;
|
||||
}
|
||||
if (attributeExists) {
|
||||
if (attributeRemoved) {
|
||||
this.delta.merge(attributeName, DeltaValue.REMOVED,
|
||||
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null : deltaValue);
|
||||
}
|
||||
else {
|
||||
this.delta.merge(attributeName, DeltaValue.UPDATED, (oldDeltaValue,
|
||||
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : deltaValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.delta.merge(attributeName, DeltaValue.ADDED, (oldDeltaValue,
|
||||
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : DeltaValue.UPDATED);
|
||||
}
|
||||
this.delegate.setAttribute(attributeName, value(attributeValue));
|
||||
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || SPRING_SECURITY_CONTEXT.equals(attributeName)) {
|
||||
this.changed = true;
|
||||
}
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
setAttribute(attributeName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.delegate.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.delegate.setLastAccessedTime(lastAccessedTime);
|
||||
this.changed = true;
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.delegate.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.delegate.setMaxInactiveInterval(interval);
|
||||
this.changed = true;
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.delegate.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.delegate.isExpired();
|
||||
}
|
||||
|
||||
private void flushIfRequired() {
|
||||
if (JdbcIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
if (this.isNew) {
|
||||
JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
|
||||
Map<String, String> indexes = JdbcIndexedSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(JdbcSession.this);
|
||||
JdbcIndexedSessionRepository.this.jdbcOperations
|
||||
.update(JdbcIndexedSessionRepository.this.createSessionQuery, (ps) -> {
|
||||
ps.setString(1, JdbcSession.this.primaryKey);
|
||||
ps.setString(2, getId());
|
||||
ps.setLong(3, getCreationTime().toEpochMilli());
|
||||
ps.setLong(4, getLastAccessedTime().toEpochMilli());
|
||||
ps.setInt(5, (int) getMaxInactiveInterval().getSeconds());
|
||||
ps.setLong(6, getExpiryTime().toEpochMilli());
|
||||
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
|
||||
});
|
||||
Set<String> attributeNames = getAttributeNames();
|
||||
if (!attributeNames.isEmpty()) {
|
||||
insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
|
||||
if (JdbcSession.this.changed) {
|
||||
Map<String, String> indexes = JdbcIndexedSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(JdbcSession.this);
|
||||
JdbcIndexedSessionRepository.this.jdbcOperations
|
||||
.update(JdbcIndexedSessionRepository.this.updateSessionQuery, (ps) -> {
|
||||
ps.setString(1, getId());
|
||||
ps.setLong(2, getLastAccessedTime().toEpochMilli());
|
||||
ps.setInt(3, (int) getMaxInactiveInterval().getSeconds());
|
||||
ps.setLong(4, getExpiryTime().toEpochMilli());
|
||||
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
|
||||
ps.setString(6, JdbcSession.this.primaryKey);
|
||||
});
|
||||
}
|
||||
List<String> addedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!addedAttributeNames.isEmpty()) {
|
||||
insertSessionAttributes(JdbcSession.this, addedAttributeNames);
|
||||
}
|
||||
List<String> updatedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!updatedAttributeNames.isEmpty()) {
|
||||
updateSessionAttributes(JdbcSession.this, updatedAttributeNames);
|
||||
}
|
||||
List<String> removedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!removedAttributeNames.isEmpty()) {
|
||||
deleteSessionAttributes(JdbcSession.this, removedAttributeNames);
|
||||
}
|
||||
});
|
||||
}
|
||||
clearChangeFlags();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {
|
||||
|
||||
@Override
|
||||
public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
|
||||
List<JdbcSession> sessions = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
String id = rs.getString("SESSION_ID");
|
||||
JdbcSession session;
|
||||
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
|
||||
session = getLast(sessions);
|
||||
}
|
||||
else {
|
||||
MapSession delegate = new MapSession(id);
|
||||
String primaryKey = rs.getString("PRIMARY_ID");
|
||||
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
|
||||
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
|
||||
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
|
||||
session = new JdbcSession(delegate, primaryKey, false);
|
||||
}
|
||||
String attributeName = rs.getString("ATTRIBUTE_NAME");
|
||||
if (attributeName != null) {
|
||||
byte[] bytes = getLobHandler().getBlobAsBytes(rs, "ATTRIBUTE_BYTES");
|
||||
session.delegate.setAttribute(attributeName, lazily(() -> deserialize(bytes)));
|
||||
}
|
||||
sessions.add(session);
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
private JdbcSession getLast(List<JdbcSession> sessions) {
|
||||
return sessions.get(sessions.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,229 +16,25 @@
|
||||
|
||||
package org.springframework.session.jdbc;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.core.serializer.support.DeserializingConverter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
import org.springframework.jdbc.support.lob.DefaultLobHandler;
|
||||
import org.springframework.jdbc.support.lob.LobHandler;
|
||||
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.SessionRepository;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionOperations;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.session.SessionRepository} implementation that uses
|
||||
* Spring's {@link JdbcOperations} to store sessions in a relational database. This
|
||||
* implementation does not support publishing of session events.
|
||||
* <p>
|
||||
* An example of how to create a new instance can be seen below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
|
||||
*
|
||||
* // ... configure jdbcTemplate ...
|
||||
*
|
||||
* TransactionTemplate transactionTemplate = new TransactionTemplate();
|
||||
*
|
||||
* // ... configure transactionTemplate ...
|
||||
*
|
||||
* JdbcOperationsSessionRepository sessionRepository =
|
||||
* new JdbcOperationsSessionRepository(jdbcTemplate, transactionTemplate);
|
||||
* </pre>
|
||||
*
|
||||
* For additional information on how to create and configure {@code JdbcTemplate} and
|
||||
* {@code TransactionTemplate}, refer to the <a href=
|
||||
* "https://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
|
||||
* Spring Framework Reference Documentation</a>.
|
||||
* <p>
|
||||
* By default, this implementation uses <code>SPRING_SESSION</code> and
|
||||
* <code>SPRING_SESSION_ATTRIBUTES</code> tables to store sessions. Note that the table
|
||||
* name can be customized using the {@link #setTableName(String)} method. In that case the
|
||||
* table used to store attributes will be named using the provided table name, suffixed
|
||||
* with <code>_ATTRIBUTES</code>.
|
||||
*
|
||||
* Depending on your database, the table definition can be described as below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* CREATE TABLE SPRING_SESSION (
|
||||
* PRIMARY_ID CHAR(36) NOT NULL,
|
||||
* SESSION_ID CHAR(36) NOT NULL,
|
||||
* CREATION_TIME BIGINT NOT NULL,
|
||||
* LAST_ACCESS_TIME BIGINT NOT NULL,
|
||||
* MAX_INACTIVE_INTERVAL INT NOT NULL,
|
||||
* EXPIRY_TIME BIGINT NOT NULL,
|
||||
* PRINCIPAL_NAME VARCHAR(100),
|
||||
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
|
||||
* );
|
||||
*
|
||||
* CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
|
||||
* CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (EXPIRY_TIME);
|
||||
* CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
|
||||
*
|
||||
* CREATE TABLE SPRING_SESSION_ATTRIBUTES (
|
||||
* SESSION_PRIMARY_ID CHAR(36) NOT NULL,
|
||||
* ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
|
||||
* ATTRIBUTE_BYTES BYTEA NOT NULL,
|
||||
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
|
||||
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
|
||||
* );
|
||||
*
|
||||
* CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_PRIMARY_ID);
|
||||
* </pre>
|
||||
*
|
||||
* Due to the differences between the various database vendors, especially when it comes
|
||||
* to storing binary data, make sure to use SQL script specific to your database. Scripts
|
||||
* for most major database vendors are packaged as
|
||||
* <code>org/springframework/session/jdbc/schema-*.sql</code>, where <code>*</code> is the
|
||||
* target database type.
|
||||
* This {@link SessionRepository} implementation is kept in order to support migration to
|
||||
* {@link JdbcIndexedSessionRepository} in a backwards compatible manner.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Craig Andrews
|
||||
* @since 1.2.0
|
||||
* @deprecated since 2.2.0 in favor of {@link JdbcIndexedSessionRepository}
|
||||
*/
|
||||
public class JdbcOperationsSessionRepository
|
||||
implements FindByIndexNameSessionRepository<JdbcOperationsSessionRepository.JdbcSession> {
|
||||
|
||||
/**
|
||||
* The default name of database table used by Spring Session to store sessions.
|
||||
*/
|
||||
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
|
||||
|
||||
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
|
||||
|
||||
// @formatter:off
|
||||
private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
|
||||
+ "SELECT PRIMARY_ID, ?, ? "
|
||||
+ "FROM %TABLE_NAME% "
|
||||
+ "WHERE SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String GET_SESSION_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
|
||||
+ "FROM %TABLE_NAME% S "
|
||||
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
|
||||
+ "WHERE S.SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? "
|
||||
+ "WHERE PRIMARY_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? "
|
||||
+ "WHERE SESSION_PRIMARY_ID = ? "
|
||||
+ "AND ATTRIBUTE_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "DELETE FROM %TABLE_NAME%_ATTRIBUTES "
|
||||
+ "WHERE SESSION_PRIMARY_ID = ? "
|
||||
+ "AND ATTRIBUTE_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% "
|
||||
+ "WHERE SESSION_ID = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
|
||||
+ "FROM %TABLE_NAME% S "
|
||||
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
|
||||
+ "WHERE S.PRINCIPAL_NAME = ?";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "DELETE FROM %TABLE_NAME% "
|
||||
+ "WHERE EXPIRY_TIME < ?";
|
||||
// @formatter:on
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JdbcOperationsSessionRepository.class);
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
|
||||
private final TransactionOperations transactionOperations;
|
||||
|
||||
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
|
||||
|
||||
private final IndexResolver<JdbcSession> indexResolver;
|
||||
|
||||
/**
|
||||
* The name of database table used by Spring Session to store sessions.
|
||||
*/
|
||||
private String tableName = DEFAULT_TABLE_NAME;
|
||||
|
||||
private String createSessionQuery;
|
||||
|
||||
private String createSessionAttributeQuery;
|
||||
|
||||
private String getSessionQuery;
|
||||
|
||||
private String updateSessionQuery;
|
||||
|
||||
private String updateSessionAttributeQuery;
|
||||
|
||||
private String deleteSessionAttributeQuery;
|
||||
|
||||
private String deleteSessionQuery;
|
||||
|
||||
private String listSessionsByPrincipalNameQuery;
|
||||
|
||||
private String deleteSessionsByExpiryTimeQuery;
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override the default value for
|
||||
* {@link JdbcSession#setMaxInactiveInterval(Duration)}.
|
||||
*/
|
||||
private Integer defaultMaxInactiveInterval;
|
||||
|
||||
private ConversionService conversionService;
|
||||
|
||||
private LobHandler lobHandler;
|
||||
|
||||
private FlushMode flushMode = FlushMode.ON_SAVE;
|
||||
|
||||
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
|
||||
@Deprecated
|
||||
public class JdbcOperationsSessionRepository extends JdbcIndexedSessionRepository {
|
||||
|
||||
/**
|
||||
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
|
||||
@@ -246,346 +42,42 @@ public class JdbcOperationsSessionRepository
|
||||
* sessions.
|
||||
* @param jdbcOperations the {@link JdbcOperations} to use
|
||||
* @param transactionOperations the {@link TransactionOperations} to use
|
||||
* @see JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations,
|
||||
* TransactionOperations)
|
||||
*/
|
||||
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations, TransactionOperations transactionOperations) {
|
||||
Assert.notNull(jdbcOperations, "jdbcOperations must not be null");
|
||||
Assert.notNull(transactionOperations, "transactionOperations must not be null");
|
||||
this.jdbcOperations = jdbcOperations;
|
||||
this.transactionOperations = transactionOperations;
|
||||
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
|
||||
this.conversionService = createDefaultConversionService();
|
||||
this.lobHandler = new DefaultLobHandler();
|
||||
prepareQueries();
|
||||
super(jdbcOperations, transactionOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
|
||||
* provided {@link JdbcOperations} to manage sessions.
|
||||
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
|
||||
* {@link JdbcOperations} to manage sessions.
|
||||
* <p>
|
||||
* The created instance will execute all data access operations in a transaction with
|
||||
* propagation level of {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW}.
|
||||
* @param jdbcOperations the {@link JdbcOperations} to use
|
||||
* @param transactionManager the {@link PlatformTransactionManager} to use
|
||||
* @deprecated since 2.2.0 in favor of
|
||||
* {@link #JdbcOperationsSessionRepository(JdbcOperations, TransactionOperations)}
|
||||
* {@link JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations, TransactionOperations)}
|
||||
*/
|
||||
@Deprecated
|
||||
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
|
||||
PlatformTransactionManager transactionManager) {
|
||||
this(jdbcOperations, createTransactionTemplate(transactionManager));
|
||||
super(jdbcOperations, createTransactionTemplate(transactionManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
|
||||
* provided {@link JdbcOperations} to manage sessions.
|
||||
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
|
||||
* {@link JdbcOperations} to manage sessions.
|
||||
* <p>
|
||||
* The created instance will not execute data access operations in a transaction.
|
||||
* @param jdbcOperations the {@link JdbcOperations} to use
|
||||
* @deprecated since 2.2.0 in favor of
|
||||
* {@link #JdbcOperationsSessionRepository(JdbcOperations, TransactionOperations)}
|
||||
* {@link JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations, TransactionOperations)}
|
||||
*/
|
||||
@Deprecated
|
||||
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
|
||||
this(jdbcOperations, TransactionOperations.withoutTransaction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of database table used to store sessions.
|
||||
* @param tableName the database table name
|
||||
*/
|
||||
public void setTableName(String tableName) {
|
||||
Assert.hasText(tableName, "Table name must not be empty");
|
||||
this.tableName = tableName.trim();
|
||||
prepareQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to create the session.
|
||||
* @param createSessionQuery the SQL query string
|
||||
*/
|
||||
public void setCreateSessionQuery(String createSessionQuery) {
|
||||
Assert.hasText(createSessionQuery, "Query must not be empty");
|
||||
this.createSessionQuery = createSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to create the session attribute.
|
||||
* @param createSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
|
||||
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
|
||||
this.createSessionAttributeQuery = createSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to retrieve the session.
|
||||
* @param getSessionQuery the SQL query string
|
||||
*/
|
||||
public void setGetSessionQuery(String getSessionQuery) {
|
||||
Assert.hasText(getSessionQuery, "Query must not be empty");
|
||||
this.getSessionQuery = getSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to update the session.
|
||||
* @param updateSessionQuery the SQL query string
|
||||
*/
|
||||
public void setUpdateSessionQuery(String updateSessionQuery) {
|
||||
Assert.hasText(updateSessionQuery, "Query must not be empty");
|
||||
this.updateSessionQuery = updateSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to update the session attribute.
|
||||
* @param updateSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
|
||||
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
|
||||
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the session attribute.
|
||||
* @param deleteSessionAttributeQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
|
||||
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
|
||||
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the session.
|
||||
* @param deleteSessionQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionQuery(String deleteSessionQuery) {
|
||||
Assert.hasText(deleteSessionQuery, "Query must not be empty");
|
||||
this.deleteSessionQuery = deleteSessionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to retrieve the sessions by principal name.
|
||||
* @param listSessionsByPrincipalNameQuery the SQL query string
|
||||
*/
|
||||
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
|
||||
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
|
||||
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom SQL query used to delete the sessions by last access time.
|
||||
* @param deleteSessionsByExpiryTimeQuery the SQL query string
|
||||
*/
|
||||
public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
|
||||
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
|
||||
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum inactive interval in seconds between requests before newly created
|
||||
* sessions will be invalidated. A negative time indicates that the session will never
|
||||
* timeout. The default is 1800 (30 minutes).
|
||||
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
public void setLobHandler(LobHandler lobHandler) {
|
||||
Assert.notNull(lobHandler, "LobHandler must not be null");
|
||||
this.lobHandler = lobHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ConversionService} to use.
|
||||
* @param conversionService the converter to set
|
||||
*/
|
||||
public void setConversionService(ConversionService conversionService) {
|
||||
Assert.notNull(conversionService, "conversionService must not be null");
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flush mode. Default is {@link FlushMode#ON_SAVE}.
|
||||
* @param flushMode the flush mode
|
||||
*/
|
||||
public void setFlushMode(FlushMode flushMode) {
|
||||
Assert.notNull(flushMode, "flushMode must not be null");
|
||||
this.flushMode = flushMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the save mode.
|
||||
* @param saveMode the save mode
|
||||
*/
|
||||
public void setSaveMode(SaveMode saveMode) {
|
||||
Assert.notNull(saveMode, "saveMode must not be null");
|
||||
this.saveMode = saveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdbcSession createSession() {
|
||||
MapSession delegate = new MapSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
|
||||
session.flushIfRequired();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(final JdbcSession session) {
|
||||
session.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdbcSession findById(final String id) {
|
||||
final JdbcSession session = this.transactionOperations.execute((status) -> {
|
||||
List<JdbcSession> sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query(
|
||||
JdbcOperationsSessionRepository.this.getSessionQuery, (ps) -> ps.setString(1, id),
|
||||
JdbcOperationsSessionRepository.this.extractor);
|
||||
if (sessions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return sessions.get(0);
|
||||
});
|
||||
|
||||
if (session != null) {
|
||||
if (session.isExpired()) {
|
||||
deleteById(id);
|
||||
}
|
||||
else {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(final String id) {
|
||||
this.transactionOperations.execute(() -> JdbcOperationsSessionRepository.this.jdbcOperations
|
||||
.update(JdbcOperationsSessionRepository.this.deleteSessionQuery, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, final String indexValue) {
|
||||
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<JdbcSession> sessions = this.transactionOperations
|
||||
.execute((status) -> JdbcOperationsSessionRepository.this.jdbcOperations.query(
|
||||
JdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
|
||||
(ps) -> ps.setString(1, indexValue), JdbcOperationsSessionRepository.this.extractor));
|
||||
|
||||
Map<String, JdbcSession> sessionMap = new HashMap<>(sessions.size());
|
||||
|
||||
for (JdbcSession session : sessions) {
|
||||
sessionMap.put(session.getId(), session);
|
||||
}
|
||||
|
||||
return sessionMap;
|
||||
}
|
||||
|
||||
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
ps.setString(1, attributeName);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
|
||||
serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(3, session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
ps.setString(1, attributeName);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(3, session.getId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
|
||||
serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(2, session.primaryKey);
|
||||
ps.setString(3, attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
|
||||
ps.setString(2, session.primaryKey);
|
||||
ps.setString(3, attributeName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
|
||||
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
|
||||
if (attributeNames.size() > 1) {
|
||||
this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
|
||||
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
String attributeName = attributeNames.get(i);
|
||||
ps.setString(1, session.primaryKey);
|
||||
ps.setString(2, attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return attributeNames.size();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.jdbcOperations.update(this.deleteSessionAttributeQuery, (ps) -> {
|
||||
String attributeName = attributeNames.get(0);
|
||||
ps.setString(1, session.primaryKey);
|
||||
ps.setString(2, attributeName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanUpExpiredSessions() {
|
||||
Integer deletedCount = this.transactionOperations
|
||||
.execute((status) -> JdbcOperationsSessionRepository.this.jdbcOperations.update(
|
||||
JdbcOperationsSessionRepository.this.deleteSessionsByExpiryTimeQuery,
|
||||
System.currentTimeMillis()));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Cleaned up " + deletedCount + " expired sessions");
|
||||
}
|
||||
super(jdbcOperations, TransactionOperations.withoutTransaction());
|
||||
}
|
||||
|
||||
private static TransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) {
|
||||
@@ -596,316 +88,4 @@ public class JdbcOperationsSessionRepository
|
||||
return transactionTemplate;
|
||||
}
|
||||
|
||||
private static GenericConversionService createDefaultConversionService() {
|
||||
GenericConversionService converter = new GenericConversionService();
|
||||
converter.addConverter(Object.class, byte[].class, new SerializingConverter());
|
||||
converter.addConverter(byte[].class, Object.class, new DeserializingConverter());
|
||||
return converter;
|
||||
}
|
||||
|
||||
private String getQuery(String base) {
|
||||
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
|
||||
}
|
||||
|
||||
private void prepareQueries() {
|
||||
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
|
||||
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
|
||||
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
|
||||
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
|
||||
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
|
||||
this.listSessionsByPrincipalNameQuery = getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
|
||||
this.deleteSessionsByExpiryTimeQuery = getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
|
||||
}
|
||||
|
||||
private LobHandler getLobHandler() {
|
||||
return this.lobHandler;
|
||||
}
|
||||
|
||||
private byte[] serialize(Object object) {
|
||||
return (byte[]) this.conversionService.convert(object, TypeDescriptor.valueOf(Object.class),
|
||||
TypeDescriptor.valueOf(byte[].class));
|
||||
}
|
||||
|
||||
private Object deserialize(byte[] bytes) {
|
||||
return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
|
||||
TypeDescriptor.valueOf(Object.class));
|
||||
}
|
||||
|
||||
private enum DeltaValue {
|
||||
|
||||
ADDED, UPDATED, REMOVED
|
||||
|
||||
}
|
||||
|
||||
private static <T> Supplier<T> value(T value) {
|
||||
return (value != null) ? () -> value : null;
|
||||
}
|
||||
|
||||
private static <T> Supplier<T> lazily(Supplier<T> supplier) {
|
||||
Supplier<T> lazySupplier = new Supplier<T>() {
|
||||
|
||||
private T value;
|
||||
|
||||
@Override
|
||||
public T get() {
|
||||
if (this.value == null) {
|
||||
this.value = supplier.get();
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (supplier != null) ? lazySupplier : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Session} to use for {@link JdbcOperationsSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
final class JdbcSession implements Session {
|
||||
|
||||
private final Session delegate;
|
||||
|
||||
private final String primaryKey;
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private boolean changed;
|
||||
|
||||
private Map<String, DeltaValue> delta = new HashMap<>();
|
||||
|
||||
JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
|
||||
this.delegate = delegate;
|
||||
this.primaryKey = primaryKey;
|
||||
this.isNew = isNew;
|
||||
if (this.isNew || (JdbcOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
|
||||
getAttributeNames().forEach((attributeName) -> this.delta.put(attributeName, DeltaValue.UPDATED));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNew() {
|
||||
return this.isNew;
|
||||
}
|
||||
|
||||
boolean isChanged() {
|
||||
return this.changed;
|
||||
}
|
||||
|
||||
Map<String, DeltaValue> getDelta() {
|
||||
return this.delta;
|
||||
}
|
||||
|
||||
void clearChangeFlags() {
|
||||
this.isNew = false;
|
||||
this.changed = false;
|
||||
this.delta.clear();
|
||||
}
|
||||
|
||||
Instant getExpiryTime() {
|
||||
return getLastAccessedTime().plus(getMaxInactiveInterval());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.delegate.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
this.changed = true;
|
||||
return this.delegate.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
Supplier<T> supplier = this.delegate.getAttribute(attributeName);
|
||||
if (supplier == null) {
|
||||
return null;
|
||||
}
|
||||
T attributeValue = supplier.get();
|
||||
if (attributeValue != null
|
||||
&& JdbcOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
|
||||
this.delta.put(attributeName, DeltaValue.UPDATED);
|
||||
}
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.delegate.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
|
||||
boolean attributeRemoved = (attributeValue == null);
|
||||
if (!attributeExists && attributeRemoved) {
|
||||
return;
|
||||
}
|
||||
if (attributeExists) {
|
||||
if (attributeRemoved) {
|
||||
this.delta.merge(attributeName, DeltaValue.REMOVED,
|
||||
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null : deltaValue);
|
||||
}
|
||||
else {
|
||||
this.delta.merge(attributeName, DeltaValue.UPDATED, (oldDeltaValue,
|
||||
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : deltaValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.delta.merge(attributeName, DeltaValue.ADDED, (oldDeltaValue,
|
||||
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : DeltaValue.UPDATED);
|
||||
}
|
||||
this.delegate.setAttribute(attributeName, value(attributeValue));
|
||||
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || SPRING_SECURITY_CONTEXT.equals(attributeName)) {
|
||||
this.changed = true;
|
||||
}
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
setAttribute(attributeName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.delegate.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.delegate.setLastAccessedTime(lastAccessedTime);
|
||||
this.changed = true;
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.delegate.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.delegate.setMaxInactiveInterval(interval);
|
||||
this.changed = true;
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.delegate.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.delegate.isExpired();
|
||||
}
|
||||
|
||||
private void flushIfRequired() {
|
||||
if (JdbcOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
if (this.isNew) {
|
||||
JdbcOperationsSessionRepository.this.transactionOperations.execute(() -> {
|
||||
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(JdbcSession.this);
|
||||
JdbcOperationsSessionRepository.this.jdbcOperations
|
||||
.update(JdbcOperationsSessionRepository.this.createSessionQuery, (ps) -> {
|
||||
ps.setString(1, JdbcSession.this.primaryKey);
|
||||
ps.setString(2, getId());
|
||||
ps.setLong(3, getCreationTime().toEpochMilli());
|
||||
ps.setLong(4, getLastAccessedTime().toEpochMilli());
|
||||
ps.setInt(5, (int) getMaxInactiveInterval().getSeconds());
|
||||
ps.setLong(6, getExpiryTime().toEpochMilli());
|
||||
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
|
||||
});
|
||||
Set<String> attributeNames = getAttributeNames();
|
||||
if (!attributeNames.isEmpty()) {
|
||||
insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
JdbcOperationsSessionRepository.this.transactionOperations.execute(() -> {
|
||||
if (JdbcSession.this.changed) {
|
||||
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
|
||||
.resolveIndexesFor(JdbcSession.this);
|
||||
JdbcOperationsSessionRepository.this.jdbcOperations
|
||||
.update(JdbcOperationsSessionRepository.this.updateSessionQuery, (ps) -> {
|
||||
ps.setString(1, getId());
|
||||
ps.setLong(2, getLastAccessedTime().toEpochMilli());
|
||||
ps.setInt(3, (int) getMaxInactiveInterval().getSeconds());
|
||||
ps.setLong(4, getExpiryTime().toEpochMilli());
|
||||
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
|
||||
ps.setString(6, JdbcSession.this.primaryKey);
|
||||
});
|
||||
}
|
||||
List<String> addedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!addedAttributeNames.isEmpty()) {
|
||||
insertSessionAttributes(JdbcSession.this, addedAttributeNames);
|
||||
}
|
||||
List<String> updatedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!updatedAttributeNames.isEmpty()) {
|
||||
updateSessionAttributes(JdbcSession.this, updatedAttributeNames);
|
||||
}
|
||||
List<String> removedAttributeNames = JdbcSession.this.delta.entrySet().stream()
|
||||
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED).map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!removedAttributeNames.isEmpty()) {
|
||||
deleteSessionAttributes(JdbcSession.this, removedAttributeNames);
|
||||
}
|
||||
});
|
||||
}
|
||||
clearChangeFlags();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {
|
||||
|
||||
@Override
|
||||
public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
|
||||
List<JdbcSession> sessions = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
String id = rs.getString("SESSION_ID");
|
||||
JdbcSession session;
|
||||
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
|
||||
session = getLast(sessions);
|
||||
}
|
||||
else {
|
||||
MapSession delegate = new MapSession(id);
|
||||
String primaryKey = rs.getString("PRIMARY_ID");
|
||||
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
|
||||
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
|
||||
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
|
||||
session = new JdbcSession(delegate, primaryKey, false);
|
||||
}
|
||||
String attributeName = rs.getString("ATTRIBUTE_NAME");
|
||||
if (attributeName != null) {
|
||||
byte[] bytes = getLobHandler().getBlobAsBytes(rs, "ATTRIBUTE_BYTES");
|
||||
session.delegate.setAttribute(attributeName, lazily(() -> deserialize(bytes)));
|
||||
}
|
||||
sessions.add(session);
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
private JdbcSession getLast(List<JdbcSession> sessions) {
|
||||
return sessions.get(sessions.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ import java.lang.annotation.Target;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
|
||||
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
|
||||
|
||||
/**
|
||||
* Qualifier annotation for a {@link DataSource} to be injected in
|
||||
* {@link JdbcOperationsSessionRepository}.
|
||||
* {@link JdbcIndexedSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.0.0
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.session.SaveMode;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
|
||||
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(JdbcHttpSessionConfiguration.class)
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public @interface EnableJdbcHttpSession {
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,7 @@ public @interface EnableJdbcHttpSession {
|
||||
* The name of database table used by Spring Session to store sessions.
|
||||
* @return the database table name
|
||||
*/
|
||||
String tableName() default JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
|
||||
String tableName() default JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME;
|
||||
|
||||
/**
|
||||
* The cron expression for expired session cleanup job. By default runs every minute.
|
||||
|
||||
@@ -45,11 +45,13 @@ 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.jdbc.JdbcOperationsSessionRepository;
|
||||
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
|
||||
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
@@ -73,15 +75,14 @@ import org.springframework.util.StringValueResolver;
|
||||
* @see EnableJdbcHttpSession
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableScheduling
|
||||
public class JdbcHttpSessionConfiguration 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 tableName = JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
|
||||
private String tableName = JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME;
|
||||
|
||||
private String cleanupCron = DEFAULT_CLEANUP_CRON;
|
||||
|
||||
@@ -95,25 +96,27 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
|
||||
private TransactionOperations transactionOperations;
|
||||
|
||||
private IndexResolver<Session> indexResolver;
|
||||
|
||||
private LobHandler lobHandler;
|
||||
|
||||
private ConversionService springSessionConversionService;
|
||||
|
||||
private ConversionService conversionService;
|
||||
|
||||
private List<SessionRepositoryCustomizer<JdbcOperationsSessionRepository>> sessionRepositoryCustomizers;
|
||||
private List<SessionRepositoryCustomizer<JdbcIndexedSessionRepository>> sessionRepositoryCustomizers;
|
||||
|
||||
private ClassLoader classLoader;
|
||||
|
||||
private StringValueResolver embeddedValueResolver;
|
||||
|
||||
@Bean
|
||||
public JdbcOperationsSessionRepository sessionRepository() {
|
||||
public JdbcIndexedSessionRepository sessionRepository() {
|
||||
JdbcTemplate jdbcTemplate = createJdbcTemplate(this.dataSource);
|
||||
if (this.transactionOperations == null) {
|
||||
this.transactionOperations = createTransactionTemplate(this.transactionManager);
|
||||
}
|
||||
JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(jdbcTemplate,
|
||||
JdbcIndexedSessionRepository sessionRepository = new JdbcIndexedSessionRepository(jdbcTemplate,
|
||||
this.transactionOperations);
|
||||
if (StringUtils.hasText(this.tableName)) {
|
||||
sessionRepository.setTableName(this.tableName);
|
||||
@@ -121,6 +124,9 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
|
||||
sessionRepository.setFlushMode(this.flushMode);
|
||||
sessionRepository.setSaveMode(this.saveMode);
|
||||
if (this.indexResolver != null) {
|
||||
sessionRepository.setIndexResolver(this.indexResolver);
|
||||
}
|
||||
if (this.lobHandler != null) {
|
||||
sessionRepository.setLobHandler(this.lobHandler);
|
||||
}
|
||||
@@ -194,6 +200,11 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
this.transactionOperations = transactionOperations;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setIndexResolver(IndexResolver<Session> indexResolver) {
|
||||
this.indexResolver = indexResolver;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@Qualifier("springSessionLobHandler")
|
||||
public void setLobHandler(LobHandler lobHandler) {
|
||||
@@ -214,7 +225,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setSessionRepositoryCustomizer(
|
||||
ObjectProvider<SessionRepositoryCustomizer<JdbcOperationsSessionRepository>> sessionRepositoryCustomizers) {
|
||||
ObjectProvider<SessionRepositoryCustomizer<JdbcIndexedSessionRepository>> sessionRepositoryCustomizers) {
|
||||
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -246,11 +257,6 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
this.saveMode = attributes.getEnum("saveMode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.addCronTask(() -> sessionRepository().cleanUpExpiredSessions(), this.cleanupCron);
|
||||
}
|
||||
|
||||
private static JdbcTemplate createJdbcTemplate(DataSource dataSource) {
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
jdbcTemplate.afterPropertiesSet();
|
||||
@@ -271,4 +277,25 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
|
||||
return conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration of scheduled job for cleaning up expired sessions.
|
||||
*/
|
||||
@EnableScheduling
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class SessionCleanupConfiguration implements SchedulingConfigurer {
|
||||
|
||||
private final JdbcIndexedSessionRepository sessionRepository;
|
||||
|
||||
SessionCleanupConfiguration(JdbcIndexedSessionRepository sessionRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.addCronTask(this.sessionRepository::cleanUpExpiredSessions,
|
||||
JdbcHttpSessionConfiguration.this.cleanupCron);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user