Compare commits

...

75 Commits

Author SHA1 Message Date
Rob Winch
357b4c8394 Release 2.0.9.RELEASE
Fixes: gh-1290
2019-01-10 21:39:58 -06:00
Rob Winch
a28bbdc997 Update to Spring Security 5.0.11.RELEASE
Fixes: gh-1293
2019-01-10 21:39:11 -06:00
Vedran Pavic
d6dc0f5cdd Update integration tests 2019-01-10 16:37:30 +01:00
Vedran Pavic
de521cd07c Upgrade test dependencies 2019-01-10 16:33:00 +01:00
Vedran Pavic
4662d5c303 Upgrade Spring Data to Kay-SR13
Resolves: #1289
2019-01-10 13:36:11 +01:00
Vedran Pavic
53de486da3 Upgrade Spring Framework to 5.0.11.RELEASE
Resolves: #1288
2019-01-09 16:23:14 +01:00
Vedran Pavic
d11e6ddfe9 Upgrade Reactor to Bismuth-SR15
Resolves: #1297
2019-01-08 21:47:35 +01:00
Vedran Pavic
6ed798e09f Fix Spring Security integration docs sample
Resolves: #1305
2019-01-08 21:46:19 +01:00
Vedran Pavic
adcfa87454 Polish 2018-12-26 20:17:14 +01:00
Vedran Pavic
c66fcf3f8b Fix assertion in Hazelcast tests 2018-12-24 21:00:23 +01:00
Vedran Pavic
ae6bc3e550 Upgrade samples to Spring Boot 2.0.7.RELEASE
Resolves: #1295
2018-12-17 21:36:21 +01:00
Roman Beskrovnyi
44f63cd301 Fix SessionRepositoryFilter Javadoc 2018-12-14 09:43:24 -06:00
Jeff
0a3dbfa43e Fix RedisOperationsSessionRepository javadoc typos
Resolves: #1275
2018-12-04 20:07:48 +01:00
Vedran Pavic
ae77a9db6c Next development version 2018-11-28 21:40:43 +01:00
Vedran Pavic
fe106ea7bb Release 2.0.8.RELEASE 2018-11-28 21:37:38 +01:00
Vedran Pavic
44ba9a97b7 Upgrade test dependencies 2018-11-28 19:23:01 +01:00
Vedran Pavic
0bdb106c30 Upgrade Spring Security to 5.0.10.RELEASE
Resolves: #1263
2018-11-28 19:22:31 +01:00
Vedran Pavic
43014247eb Upgrade Spring Data to Kay-SR12
Resolves: #1247
2018-11-27 15:11:13 +01:00
Vedran Pavic
7e8917ac47 Upgrade Spring Framework to 5.0.11.RELEASE
Resolves: #1246
2018-11-27 12:34:04 +01:00
Vedran Pavic
ad90867590 Polish 2018-11-26 19:10:13 +01:00
Vedran Pavic
32c28013f3 Make SessionUpdateEntryProcessor public
Resolves: #1265
2018-11-26 19:09:03 +01:00
Vedran Pavic
c5b43f096c Update integration tests 2018-11-26 19:07:20 +01:00
Vedran Pavic
82759642c3 Upgrade test dependencies 2018-11-26 18:59:40 +01:00
Vedran Pavic
74c5260754 Upgrade Reactor to Bismuth-SR14
Resolves: #1264
2018-11-23 19:35:01 +01:00
Vedran Pavic
55b4f6f017 Update integration tests 2018-11-02 23:15:25 +01:00
Vedran Pavic
9099bd5d3a Upgrade test dependencies 2018-11-02 23:15:13 +01:00
Vedran Pavic
afa1f0890e Upgrade samples to Spring Boot 2.0.6.RELEASE
Resolves: #1245
2018-11-02 22:59:28 +01:00
Vedran Pavic
ed3f6abf5d Polish contribution
Resolves: #1244
2018-11-02 22:51:14 +01:00
Josh Cummings
6c322631d4 Commit Session on Include Dispatch
The servlet spec disallows any writing of headers after an include has been issued.

This commit intercepts the include and commits the session, then
allowing the include to proceed.

See: #1244
2018-11-02 22:50:28 +01:00
Vedran Pavic
9575be9b7d Ensure HttpServletRequest#getRequestedSessionId API is respected
HttpSessionIdResolver supports resolving multiple requested session ids associated with the request - as a consequence, we need to validate the existence of requested session before returning the id. However, if no presented session ids do validate the null is returned, which violates the HttpServletRequest#getRequestedSessionId API.

This commit ensures that if no presented session ids are valid, we respect the HttpServletRequest#getRequestedSessionId API by returning first requested session id.

Resolves: #1236
2018-10-26 20:12:39 +02:00
Rob Winch
eae239febf Next Development Version 2018-10-15 20:12:43 -05:00
Rob Winch
b86b34ca2e Release 2.0.7.RELEASE 2018-10-15 20:11:03 -05:00
Rob Winch
c0a2220d3b Update to Spring Security 5.0.9.RELEASE
Fixes: gh-1226
2018-10-15 20:09:51 -05:00
Vedran Pavic
c9d6ef7f01 Upgrade integration tests 2018-10-15 19:59:54 +02:00
Vedran Pavic
c2c1311830 Upgrade test dependencies 2018-10-15 19:59:35 +02:00
Vedran Pavic
8c97a73b36 Upgrade Reactor to Bismuth-SR12
Resolves: #1225
2018-10-15 19:50:20 +02:00
Vedran Pavic
0886e237b6 Upgrade Spring Data to Kay-SR11
Resolves: #1224
2018-10-15 19:48:24 +02:00
Vedran Pavic
c57a286e35 Upgrade Spring Framework to 5.0.10.RELEASE
Resolves: #1209
2018-10-15 19:45:44 +02:00
Vedran Pavic
4c5f22900d Polish 2018-09-26 14:47:53 +02:00
Vedran Pavic
8a8f379b37 Disable network join in Hazelcast samples 2018-09-26 14:47:52 +02:00
Vedran Pavic
dc4a0ce61b Configure default LobHandler to use temporary LOBs on Oracle
JdbcOperationsSessionRepository recently introduced validation when inserting new session attributes in order to prevent data integrity violations in highly concurrent environments. This is done by using INSERT INTO ... SELECT statement to verify existence of session record in parent table. Such arrangement causes problems with Oracle if inserted attribute is of size 4 kb or more.

This commit enhances JdbcHttpSessionConfiguration to detect Oracle database is used, and set createTemporaryLob option on default LobHandler to true.

Resolves: #1208
2018-09-25 19:30:09 +02:00
Vedran Pavic
5944648c25 Fix SpringSessionRememberMeServices documentation example
Resolves: #1210
2018-09-25 19:27:39 +02:00
Vedran Pavic
4502724e8c Upgrade samples to Spring Boot 2.0.5.RELEASE
Closes gh-1194
2018-09-13 21:43:47 +02:00
Vedran Pavic
3c8cce652e Upgrade Gradle to 4.10.1 2018-09-13 21:43:11 +02:00
Vedran Pavic
425df2261f Update Jenkinsfile to specify node label 2018-09-13 18:24:10 +02:00
Vedran Pavic
7c6b143964 Ensure RedisHttpSessionConfiguration handles events for configured database
At present, RedisHttpSessionConfiguration doesn't take into account database index when handlng events. In situations where multiple apps use Spring Session with same Redis instance, but different database, this results in invalid session events.

This commits improves event handling in RedisHttpSessionConfiguration to ensure currently used database is considered.

Closes gh-1193
2018-09-13 18:23:59 +02:00
Vedran Pavic
39c640f456 Next development version 2018-09-11 00:21:34 +02:00
Vedran Pavic
f190da3757 Release 2.0.6.RELEASE 2018-09-11 00:19:56 +02:00
Vedran Pavic
34060cf0f6 Upgrade Spring Security to 5.0.8.RELEASE
Closes gh-1178
2018-09-10 23:00:13 +02:00
Vedran Pavic
ff10709f18 Polish
See gh-1185
2018-09-10 17:16:31 +02:00
Vedran Pavic
6e471f6441 Upgrade test dependencies 2018-09-10 17:16:12 +02:00
Vedran Pavic
a03be43450 Upgrade Spring Data to Kay-SR10
Closes gh-1186
2018-09-10 16:38:21 +02:00
Vedran Pavic
7e8f500df0 Verify session existence before update in ReactiveRedisOperationsSessionRepository
Currently, ReactiveRedisOperationsSessionRepository#save does not ensure session's existence before executing update. This can result in an invalid session record in Redis, since write use only delta, and in turn to error while retrieving the invalid session record.

This commit adds check for session existence if session is being updated.

Closes gh-1185
2018-09-10 00:04:06 +02:00
Vedran Pavic
b27742ce3e Upgrade Spring Framework to 5.0.9.RELEASE
Closes gh-1152
2018-09-07 18:09:27 +02:00
Vedran Pavic
af44e71af0 Update integration tests 2018-09-06 18:05:32 +02:00
Vedran Pavic
055c2bcb93 Upgrade test dependencies 2018-09-06 17:57:19 +02:00
Vedran Pavic
3335deb5d5 Upgrade Reactor to Bismuth-SR11
Closes gh-1180
2018-09-06 07:40:45 +02:00
Vedran Pavic
1079e9e016 Ignore failed rename operation for deleted session
Attempting to change session id for a deleted session currently results in "ERR no such key" error on rename operation of expired key. This commit addressed the problem by ignoring the aforementioned error.

Closes #1177
2018-09-04 23:18:07 +02:00
Vedran Pavic
5fa52be8d1 Upgrade Gradle to 4.10 2018-09-04 23:17:46 +02:00
Vedran Pavic
c90952031f Fix Jenkinsfile 2018-08-27 10:16:17 +02:00
Vedran Pavic
ce308ca513 Fix Jenkinsfile 2018-08-27 08:43:01 +02:00
Vedran Pavic
c8f78e510e Upgrade spring-build-conventions to 0.0.18.RELEASE 2018-08-24 23:59:12 +02:00
Rob Winch
748ba70a01 Fix settings.gradle on Windows
Fixes: gh-1169
2018-08-22 20:36:08 +02:00
Vedran Pavic
dc1c7cdf02 Polish 2018-08-13 08:36:23 +02:00
Vedran Pavic
3a972bef76 Insert new attributes conditionally in JDBC repo
At present, the insert of new attributes in JdbcOperationsSessionRepository is done unconditionally. This can cause data integrity violation errors with concurrent requests, where one request attempts to add new session attribute while the other, concurrent request, deletes the session.

This commit addresses the described scenario by executing insert of new attributes conditionally on presence of parent record.

Closes gh-1151
2018-08-13 08:36:08 +02:00
Vedran Pavic
6b7fc3af08 Fix Jenkinsfile 2018-08-01 11:01:44 +02:00
Vedran Pavic
535160bc92 Update Jenkinsfile
- set check stage timeout to 30 minutes
- set build discared to keep last 10 builds
- handle deploy stage errors
- general formatting improvements
2018-08-01 10:54:45 +02:00
Vedran Pavic
606e08007e Upgrade samples to Spring Boot 2.0.4.RELEASE
Closes gh-1138
2018-07-31 17:10:12 +02:00
Vedran Pavic
06fa33e48b Next development version 2018-07-29 09:54:16 +02:00
Vedran Pavic
d7c2e8e79c Release 2.0.5.RELEASE 2018-07-29 09:48:45 +02:00
Vedran Pavic
6bec95a298 Polish 2018-07-27 13:27:27 +02:00
Vedran Pavic
9249a140c9 Upgrade dependencies 2018-07-27 11:05:33 +02:00
Vedran Pavic
7f6dc801e0 Upgrade Spring Data to Kay-SR9
Closes gh-1122
2018-07-27 10:23:12 +02:00
Vedran Pavic
83d46ad685 Upgrade Spring Security to 5.0.7.RELEASE
Closes gh-1123
2018-07-27 01:14:21 +02:00
Vedran Pavic
21cef2b7fa Upgrade Spring Framework to 5.0.8.RELEASE
Closes gh-1121
2018-07-26 23:25:13 +02:00
62 changed files with 616 additions and 258 deletions

92
Jenkinsfile vendored
View File

@@ -1,9 +1,9 @@
def projectProperties = [
[$class: 'BuildDiscarderProperty',
strategy: [$class: 'LogRotator', numToKeepStr: '5']],
pipelineTriggers([cron('@daily')])
]
properties(projectProperties)
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
pipelineTriggers([
cron('@daily')
]),
])
def SUCCESS = hudson.model.Result.SUCCESS.toString()
currentBuild.result = SUCCESS
@@ -11,15 +11,19 @@ currentBuild.result = SUCCESS
try {
parallel check: {
stage('Check') {
node {
checkout scm
try {
sh "./gradlew clean check --refresh-dependencies --no-daemon"
} catch(Exception e) {
currentBuild.result = 'FAILED: check'
throw e
} finally {
junit '**/build/*-results/*.xml'
timeout(time: 30, unit: 'MINUTES') {
node('ubuntu1804') {
checkout scm
try {
sh './gradlew clean check --no-daemon --refresh-dependencies'
}
catch (e) {
currentBuild.result = 'FAILED: check'
throw e
}
finally {
junit '**/build/test-results/*/*.xml'
}
}
}
}
@@ -29,31 +33,39 @@ try {
node {
checkout scm
try {
sh "./gradlew clean springIoCheck -PplatformVersion=Cairo-BUILD-SNAPSHOT -PexcludeProjects='**/samples/**' --refresh-dependencies --no-daemon --stacktrace"
} catch(Exception e) {
sh "./gradlew clean springIoCheck --stacktrace --no-daemon --refresh-dependencies -PplatformVersion=Cairo-BUILD-SNAPSHOT -PexcludeProjects='**/samples/**'"
}
catch(e) {
currentBuild.result = 'FAILED: springio'
throw e
} finally {
}
finally {
junit '**/build/spring-io*-results/*.xml'
}
}
}
}
if(currentBuild.result == 'SUCCESS') {
if (currentBuild.result == 'SUCCESS') {
parallel artifacts: {
stage('Deploy Artifacts') {
node {
checkout scm
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')]) {
sh "./gradlew deployArtifacts finalizeDeployArtifacts -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 --refresh-dependencies --no-daemon --stacktrace"
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')]) {
sh './gradlew deployArtifacts finalizeDeployArtifacts --stacktrace --no-daemon --refresh-dependencies -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
}
}
}
},
@@ -61,32 +73,38 @@ try {
stage('Deploy Docs') {
node {
checkout scm
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
sh "./gradlew deployDocs -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME --refresh-dependencies --no-daemon --stacktrace"
try {
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
sh './gradlew deployDocs --stacktrace --no-daemon --refresh-dependencies -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME'
}
}
catch (e) {
currentBuild.result = 'FAILED: docs'
throw e
}
}
}
}
}
} finally {
}
finally {
def buildStatus = currentBuild.result
def buildNotSuccess = !SUCCESS.equals(buildStatus)
def buildNotSuccess = !SUCCESS.equals(buildStatus)
def lastBuildNotSuccess = !SUCCESS.equals(currentBuild.previousBuild?.result)
if(buildNotSuccess || lastBuildNotSuccess) {
stage('Notifiy') {
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}"""
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"
emailext(
subject: subject,
body: details,
recipientProviders: RECIPIENTS,
to: "$SPRING_SESSION_TEAM_EMAILS"
)
}
}

View File

@@ -1,6 +1,6 @@
buildscript {
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.17.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.18.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
repositories {

View File

@@ -24,7 +24,6 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
@@ -54,7 +53,7 @@ public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapte
// tag::rememberme-bean[]
@Bean
RememberMeServices rememberMeServices() {
public SpringSessionRememberMeServices rememberMeServices() {
SpringSessionRememberMeServices rememberMeServices =
new SpringSessionRememberMeServices();
// optionally customize

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 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.
@@ -30,10 +30,11 @@ import org.springframework.session.security.SpringSessionBackedSessionRegistry;
*/
// tag::class[]
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public class SecurityConfiguration<S extends Session>
extends WebSecurityConfigurerAdapter {
@Autowired
private FindByIndexNameSessionRepository<Session> sessionRepository;
private FindByIndexNameSessionRepository<S> sessionRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
@@ -47,7 +48,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
@Bean
SpringSessionBackedSessionRegistry sessionRegistry() {
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
}

View File

@@ -4,7 +4,7 @@
<module name="Checker">
<!-- Supressions -->
<module name="SuppressionFilter">
<property name="file" value="${configDir}/suppressions.xml"/>
<property name="file" value="${config_loc}/suppressions.xml"/>
</module>
<!-- Root Checks -->

View File

@@ -1,16 +0,0 @@
^\Q/*\E$
^\Q * Copyright 2014-\E20\d\d\Q the original author or authors.\E$
^\Q *\E$
^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$
^\Q * you may not use this file except in compliance with the License.\E$
^\Q * You may obtain a copy of the License at\E$
^\Q *\E$
^\Q * http://www.apache.org/licenses/LICENSE-2.0\E$
^\Q *\E$
^\Q * Unless required by applicable law or agreed to in writing, software\E$
^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$
^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$
^\Q * See the License for the specific language governing permissions and\E$
^\Q * limitations under the License.\E$
^\Q */\E$
^.*$

View File

@@ -7,7 +7,6 @@
<!-- docs -->
<suppress files="[\\/]docs[\\/]" checks="Javadoc*"/>
<suppress files="[\\/]docs[\\/]" checks="AvoidStaticImport"/>
<suppress files="[\\/]docs[\\/]" checks="InnerTypeLast"/>
<!-- samples -->

View File

@@ -1,2 +1,2 @@
springBootVersion=2.0.3.RELEASE
version=2.0.5.BUILD-SNAPSHOT
springBootVersion=2.0.7.RELEASE
version=2.0.9.RELEASE

View File

@@ -1,11 +1,11 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
mavenBom 'io.projectreactor:reactor-bom:Bismuth-SR10'
mavenBom 'org.springframework:spring-framework-bom:5.0.7.RELEASE'
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-SR8'
mavenBom 'org.springframework.security:spring-security-bom:5.0.6.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.8.1'
mavenBom 'io.projectreactor:reactor-bom:Bismuth-SR15'
mavenBom 'org.springframework:spring-framework-bom:5.0.12.RELEASE'
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-SR13'
mavenBom 'org.springframework.security:spring-security-bom:5.0.11.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.10.5'
}
dependencies {
@@ -15,17 +15,18 @@ dependencyManagement {
}
dependency 'com.h2database:h2:1.4.197'
dependency 'com.microsoft.sqlserver:mssql-jdbc:6.4.0.jre8'
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.0.0.jre8'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.0.4.RELEASE'
dependency 'io.lettuce:lettuce-core:5.1.3.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:3.1.0'
dependency 'junit:junit:4.12'
dependency 'mysql:mysql-connector-java:8.0.11'
dependency 'mysql:mysql-connector-java:8.0.13'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.10.0'
dependency 'org.assertj:assertj-core:3.11.1'
dependency 'org.hsqldb:hsqldb:2.4.1'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.2.5'
dependency 'org.mockito:mockito-core:2.18.3'
dependency 'org.postgresql:postgresql:42.2.2'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.3.0'
dependency 'org.mockito:mockito-core:2.23.4'
dependency 'org.postgresql:postgresql:42.2.5'
}
}

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -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.
@@ -46,7 +46,7 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
public class FindByUsernameTests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Autowired
private MockMvc mockMvc;

View File

@@ -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.
@@ -50,7 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureMockMvc
public class HttpRedisJsonTest {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Autowired
private MockMvc mockMvc;

View File

@@ -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.
@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class RedisSerializerTest {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@SpringSessionRedisOperations
private RedisTemplate<Object, Object> sessionRedisTemplate;

View File

@@ -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.
@@ -45,7 +45,7 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
public class BootTests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Autowired
private MockMvc mockMvc;

View File

@@ -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.
@@ -47,7 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class AttributeTests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@LocalServerPort
private int port;

View File

@@ -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.
@@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Value("${local.server.port}")
private String port;

View File

@@ -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.
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Bean
public GenericContainer redisContainer() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2018 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.
@@ -42,7 +42,8 @@ public class SessionConfig {
int port = SocketUtils.findAvailableTcpPort();
config.getNetworkConfig()
.setPort(port);
.setPort(port)
.getJoin().getMulticastConfig().setEnabled(false);
System.out.println("Hazelcast port #: " + port);

View File

@@ -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.
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Bean
public GenericContainer redisContainer() {

View File

@@ -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.
@@ -54,7 +54,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebAppConfiguration
public class RestMockMvcTests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Autowired
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;

View File

@@ -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.
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Bean
public GenericContainer redisContainer() {

View File

@@ -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.
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Bean
public GenericContainer redisContainer() {

View File

@@ -64,7 +64,9 @@ public class Initializer implements ServletContextListener {
private HazelcastInstance createHazelcastInstance() {
Config config = new Config();
config.getNetworkConfig().setPort(getAvailablePort());
config.getNetworkConfig()
.setPort(getAvailablePort())
.getJoin().getMulticastConfig().setEnabled(false);
config.getMapConfig(SESSION_MAP_NAME)
.setTimeToLiveSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);

View File

@@ -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.
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
@Bean
public GenericContainer redisContainer() {

View File

@@ -10,7 +10,7 @@ String rootDirPath = rootDir.absolutePath + File.separator
buildFiles.each { File buildFile ->
if (buildFile.name == 'build.gradle') {
String buildFilePath = buildFile.parentFile.absolutePath
String projectPath = buildFilePath.replace(rootDirPath, '').replaceAll(File.separator, ':')
String projectPath = buildFilePath.replace(rootDirPath, '').replace(File.separator, ':')
include projectPath
}
else {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -81,7 +81,7 @@ public interface Session {
@SuppressWarnings("unchecked")
default <T> T getAttributeOrDefault(String name, T defaultValue) {
T result = getAttribute(name);
return (result != null ? result : defaultValue);
return (result != null) ? result : defaultValue;
}
/**

View File

@@ -110,8 +110,9 @@ public class SpringHttpSessionConfiguration implements ApplicationContextAware {
@PostConstruct
public void init() {
CookieSerializer cookieSerializer = (this.cookieSerializer != null
? this.cookieSerializer : createDefaultCookieSerializer());
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}

View File

@@ -102,8 +102,9 @@ public class DefaultCookieSerializer implements CookieSerializer {
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = (this.jvmRoute != null
? requestedCookieValue + this.jvmRoute : requestedCookieValue);
String actualCookieValue = (this.jvmRoute != null)
? requestedCookieValue + this.jvmRoute
: requestedCookieValue;
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding
? base64Encode(actualCookieValue) : actualCookieValue);

View File

@@ -98,8 +98,8 @@ public class HeaderHttpSessionIdResolver implements HttpSessionIdResolver {
@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
String headerValue = request.getHeader(this.headerName);
return (headerValue != null ? Collections.singletonList(headerValue)
: Collections.emptyList());
return (headerValue != null) ? Collections.singletonList(headerValue)
: Collections.emptyList();
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2018 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.
@@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletResponse;
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
* @since 2.0.0
*/
public interface HttpSessionIdResolver {

View File

@@ -174,11 +174,11 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
}
private void trackContentLength(byte[] content) {
checkContentLength(content != null ? content.length : 0);
checkContentLength((content != null) ? content.length : 0);
}
private void trackContentLength(char[] content) {
checkContentLength(content != null ? content.length : 0);
checkContentLength((content != null) ? content.length : 0);
}
private void trackContentLength(int content) {
@@ -257,13 +257,13 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
}
@Override
public int hashCode() {
return this.delegate.hashCode();
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
@Override
public boolean equals(Object obj) {
return this.delegate.equals(obj);
public int hashCode() {
return this.delegate.hashCode();
}
@Override
@@ -502,13 +502,13 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
}
@Override
public int hashCode() {
return this.delegate.hashCode();
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
@Override
public boolean equals(Object obj) {
return this.delegate.equals(obj);
public int hashCode() {
return this.delegate.hashCode();
}
@Override

View File

@@ -21,8 +21,11 @@ import java.time.Instant;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
@@ -46,7 +49,7 @@ import org.springframework.session.SessionRepository;
* {@link org.springframework.session.SessionRepository}.
*
* The {@link SessionRepositoryFilter} uses a {@link HttpSessionIdResolver} (default
* {@link CookieHttpSessionIdResolver} to bridge logic between an
* {@link CookieHttpSessionIdResolver}) to bridge logic between an
* {@link javax.servlet.http.HttpSession} and the
* {@link org.springframework.session.Session} abstraction. Specifically:
*
@@ -71,6 +74,7 @@ import org.springframework.session.SessionRepository;
* @since 1.0
* @author Rob Winch
* @author Vedran Pavic
* @author Josh Cummings
*/
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@@ -205,6 +209,8 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
private boolean requestedSessionCached;
private String requestedSessionId;
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
@@ -277,7 +283,6 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
}
return isRequestedSessionIdValid(requestedSession);
}
return this.requestedSessionIdValid;
}
@@ -351,8 +356,16 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
@Override
public String getRequestedSessionId() {
S requestedSession = getRequestedSession();
return (requestedSession != null ? requestedSession.getId() : null);
if (this.requestedSessionId == null) {
getRequestedSession();
}
return this.requestedSessionId;
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
RequestDispatcher requestDispatcher = super.getRequestDispatcher(path);
return new SessionCommittingRequestDispatcher(requestDispatcher);
}
private S getRequestedSession() {
@@ -360,10 +373,14 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
S session = SessionRepositoryFilter.this.sessionRepository
.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
@@ -375,6 +392,7 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
private void clearRequestedSessionCache() {
this.requestedSessionCached = false;
this.requestedSession = null;
this.requestedSessionId = null;
}
/**
@@ -399,6 +417,35 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
}
}
/**
* Ensures session is committed before issuing an include.
*
* @since 1.3.4
*/
private final class SessionCommittingRequestDispatcher
implements RequestDispatcher {
private final RequestDispatcher delegate;
SessionCommittingRequestDispatcher(RequestDispatcher delegate) {
this.delegate = delegate;
}
@Override
public void forward(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
this.delegate.forward(request, response);
}
@Override
public void include(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
SessionRepositoryRequestWrapper.this.commitSession();
this.delegate.include(request, response);
}
}
}
}

View File

@@ -71,9 +71,9 @@ public final class WebSocketRegistryListener
SessionDisconnectEvent e = (SessionDisconnectEvent) event;
Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor
.getSessionAttributes(e.getMessage().getHeaders());
String httpSessionId = (sessionAttributes != null
String httpSessionId = (sessionAttributes != null)
? SessionRepositoryMessageInterceptor.getSessionId(sessionAttributes)
: null);
: null;
afterConnectionClosed(httpSessionId, e.getSessionId());
}
}

View File

@@ -117,8 +117,9 @@ public final class SessionRepositoryMessageInterceptor<S extends Session>
}
Map<String, Object> sessionHeaders = SimpMessageHeaderAccessor
.getSessionAttributes(message.getHeaders());
String sessionId = (sessionHeaders != null
? (String) sessionHeaders.get(SPRING_SESSION_ID_ATTR_NAME) : null);
String sessionId = (sessionHeaders != null)
? (String) sessionHeaders.get(SPRING_SESSION_ID_ATTR_NAME)
: null;
if (sessionId != null) {
S session = this.sessionRepository.findById(sessionId);
if (session != null) {

View File

@@ -1166,6 +1166,23 @@ public class SessionRepositoryFilterTests {
});
}
@Test // gh-1243
public void doFilterInclude() throws Exception {
doFilter(new DoInFilter() {
@Override
public void doFilter(HttpServletRequest wrappedRequest,
HttpServletResponse wrappedResponse)
throws IOException, ServletException {
String id = wrappedRequest.getSession().getId();
wrappedRequest.getRequestDispatcher("/").include(wrappedRequest,
wrappedResponse);
assertThat(
SessionRepositoryFilterTests.this.sessionRepository.findById(id))
.isNotNull();
}
});
}
// --- HttpSessionIdResolver
@Test
@@ -1192,6 +1209,29 @@ public class SessionRepositoryFilterTests {
});
}
@Test // gh-1229
public void doFilterAdapterGetRequestedSessionIdForInvalidSession() throws Exception {
SessionRepository<MapSession> sessionRepository = new MapSessionRepository(
new HashMap<>());
this.filter = new SessionRepositoryFilter<>(sessionRepository);
this.filter.setHttpSessionIdResolver(this.strategy);
final String expectedId = "HttpSessionIdResolver-requested-id1";
final String otherId = "HttpSessionIdResolver-requested-id2";
given(this.strategy.resolveSessionIds(any(HttpServletRequest.class)))
.willReturn(Arrays.asList(expectedId, otherId));
doFilter(new DoInFilter() {
@Override
public void doFilter(HttpServletRequest wrappedRequest,
HttpServletResponse wrappedResponse) {
assertThat(wrappedRequest.getRequestedSessionId()).isEqualTo(expectedId);
assertThat(wrappedRequest.isRequestedSessionIdValid()).isFalse();
}
});
}
@Test
public void doFilterAdapterOnNewSession() throws Exception {
this.filter.setHttpSessionIdResolver(this.strategy);

View File

@@ -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.
@@ -29,7 +29,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:4.0.10";
private static final String DOCKER_IMAGE = "redis:4.0.12";
protected static class BaseConfig {

View File

@@ -16,6 +16,8 @@
package org.springframework.session.data.redis;
import java.time.Instant;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -191,6 +193,28 @@ public class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedi
assertThat(this.repository.findById(originalId).block()).isNull();
}
// gh-1111
@Test
public void changeSessionSaveOldSessionInstance() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository
.createSession().block();
String sessionId = toSave.getId();
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository
.findById(sessionId).block();
session.changeSessionId();
session.setLastAccessedTime(Instant.now());
this.repository.save(session).block();
toSave.setLastAccessedTime(Instant.now());
this.repository.save(toSave).block();
assertThat(this.repository.findById(sessionId).block()).isNull();
assertThat(this.repository.findById(session.getId()).block()).isNotNull();
}
@Configuration
@EnableRedisWebSession
static class Config extends BaseConfig {

View File

@@ -16,6 +16,7 @@
package org.springframework.session.data.redis;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;
@@ -190,9 +191,10 @@ public class RedisOperationsSessionRepositoryITests extends AbstractRedisITests
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:"
+ toSave.getId();
String channel = ":expired";
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"),
body.getBytes("UTF-8"));
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
byte[] pattern = new byte[] {};
this.repository.onMessage(message, pattern);
@@ -358,9 +360,10 @@ public class RedisOperationsSessionRepositoryITests extends AbstractRedisITests
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:"
+ toSave.getId();
String channel = ":expired";
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"),
body.getBytes("UTF-8"));
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
byte[] pattern = new byte[] {};
this.repository.onMessage(message, pattern);
@@ -581,6 +584,22 @@ public class RedisOperationsSessionRepositoryITests extends AbstractRedisITests
assertThat(this.repository.findById(originalId)).isNull();
}
// gh-1137
@Test
public void changeSessionIdWhenSessionIsDeleted() {
RedisSession toSave = this.repository.createSession();
String sessionId = toSave.getId();
this.repository.save(toSave);
this.repository.deleteById(sessionId);
toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(toSave.getId())).isNull();
assertThat(this.repository.findById(sessionId)).isNull();
}
private String getSecurityName() {
return this.context.getAuthentication().getName();
}

View File

@@ -95,7 +95,7 @@ public class RedisListenerContainerTaskExecutorITests extends AbstractRedisITest
synchronized (this.lock) {
this.lock.wait(TimeUnit.SECONDS.toMillis(1));
}
return (this.taskDispatched != null ? this.taskDispatched : Boolean.FALSE);
return (this.taskDispatched != null) ? this.taskDispatched : Boolean.FALSE;
}
}

View File

@@ -134,24 +134,36 @@ public class ReactiveRedisOperationsSessionRepository implements
@Override
public Mono<Void> save(RedisSession session) {
return session.saveDelta().and((s) -> {
if (session.isNew) {
session.setNew(false);
}
s.onComplete();
});
Mono<Void> result = session.saveChangeSessionId().and(session.saveDelta())
.and((s) -> {
session.isNew = false;
s.onComplete();
});
if (session.isNew) {
return result;
}
else {
String sessionKey = getSessionKey(
session.hasChangedSessionId() ? session.originalSessionId
: session.getId());
return this.sessionRedisOperations.hasKey(sessionKey)
.flatMap((exists) -> exists ? result : Mono.empty());
}
}
@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 SessionMapper(id))
.filter((session) -> !session.isExpired()).map(RedisSession::new)
.filter((map) -> !map.isEmpty())
.map(new SessionMapper(id))
.filter((session) -> !session.isExpired())
.map(RedisSession::new)
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@Override
@@ -276,12 +288,8 @@ public class ReactiveRedisOperationsSessionRepository implements
return this.cached.isExpired();
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
public boolean isNew() {
return this.isNew;
private boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
private void flushImmediateIfNecessary() {
@@ -296,38 +304,35 @@ public class ReactiveRedisOperationsSessionRepository implements
}
private Mono<Void> saveDelta() {
String sessionId = getId();
Mono<Void> changeSessionId = saveChangeSessionId(sessionId);
if (this.delta.isEmpty()) {
return changeSessionId.and(Mono.empty());
return Mono.empty();
}
String sessionKey = getSessionKey(sessionId);
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.opsForHash().putAll(sessionKey, this.delta);
Mono<Boolean> setTtl = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.expire(sessionKey, getMaxInactiveInterval());
return changeSessionId.and(update).and(setTtl).and((s) -> {
return update.and(setTtl).and((s) -> {
this.delta.clear();
s.onComplete();
}).then();
}
private Mono<Void> saveChangeSessionId(String sessionId) {
if (sessionId.equals(this.originalSessionId)) {
private Mono<Void> saveChangeSessionId() {
if (!hasChangedSessionId()) {
return Mono.empty();
}
String sessionId = getId();
Publisher<Void> replaceSessionId = (s) -> {
this.originalSessionId = sessionId;
s.onComplete();
};
if (isNew()) {
if (this.isNew) {
return Mono.from(replaceSessionId);
}
else {

View File

@@ -28,6 +28,8 @@ 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;
@@ -200,7 +202,7 @@ import org.springframework.util.Assert;
*
* <p>
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if they key has not been accessed.
* 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="http://redis.io/topics/notifications">Timing of expired events</a> section in
@@ -211,7 +213,7 @@ import org.springframework.util.Assert;
* 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 they key.
* access the key.
* </p>
*
* <p>
@@ -252,6 +254,11 @@ public class RedisOperationsSessionRepository implements
static PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
/**
* 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.
*/
@@ -284,11 +291,19 @@ public class RedisOperationsSessionRepository implements
*/
static final String SESSION_ATTR_PREFIX = "sessionAttr:";
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;
@@ -325,6 +340,7 @@ public class RedisOperationsSessionRepository implements
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
this::getExpirationsKey, this::getSessionKey);
configureSessionChannels();
}
/**
@@ -375,6 +391,27 @@ public class RedisOperationsSessionRepository implements
this.redisFlushMode = redisFlushMode;
}
/**
* 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;
}
@@ -495,7 +532,7 @@ public class RedisOperationsSessionRepository implements
String channel = new String(messageChannel);
if (channel.startsWith(getSessionCreatedChannelPrefix())) {
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
.deserialize(message.getBody());
@@ -508,8 +545,8 @@ public class RedisOperationsSessionRepository implements
return;
}
boolean isDeleted = channel.endsWith(":del");
if (isDeleted || channel.endsWith(":expired")) {
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);
@@ -572,6 +609,7 @@ public class RedisOperationsSessionRepository implements
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
configureSessionChannels();
}
/**
@@ -603,17 +641,33 @@ public class RedisOperationsSessionRepository implements
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:" + "expires:";
return this.namespace + "sessions:expires:";
}
/**
* Gets the prefix for the channel that SessionCreatedEvent are published to. The
* suffix is the session id of the session that was created.
*
* @return the prefix for the channel that SessionCreatedEvent are published to
* 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.namespace + "event:created:";
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;
}
/**
@@ -797,9 +851,10 @@ public class RedisOperationsSessionRepository implements
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli()
: null);
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval())
.toEpochMilli()
: null;
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
}
@@ -813,8 +868,16 @@ public class RedisOperationsSessionRepository implements
originalSessionIdKey, sessionIdKey);
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(
originalExpiredKey, expiredKey);
try {
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(
originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils
.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
this.originalSessionId = sessionId;
}

View File

@@ -37,7 +37,10 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
@@ -54,6 +57,7 @@ import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -115,6 +119,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setRedisFlushMode(this.redisFlushMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
}
@@ -128,9 +134,9 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository(),
Arrays.asList(new PatternTopic("__keyevent@*:del"),
new PatternTopic("__keyevent@*:expired")));
container.addMessageListener(sessionRepository(), Arrays.asList(
new ChannelTopic(sessionRepository().getSessionDeletedChannel()),
new ChannelTopic(sessionRepository().getSessionExpiredChannel())));
container.addMessageListener(sessionRepository(),
Collections.singletonList(new PatternTopic(
sessionRepository().getSessionCreatedChannelPrefix() + "*")));
@@ -256,6 +262,18 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.

View File

@@ -183,6 +183,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
@Test
public void saveSessionNothingChanged() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any()))
.willReturn(Mono.just(true));
@@ -191,12 +192,14 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
StepVerifier.create(this.repository.save(session)).verifyComplete();
verify(this.redisOperations).hasKey(anyString());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
}
@Test
public void saveLastAccessChanged() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any()))
@@ -206,6 +209,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
session.setLastAccessedTime(Instant.ofEpochMilli(12345678L));
Mono.just(session).subscribe(this.repository::save);
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
@@ -219,6 +223,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
@Test
public void saveSetAttribute() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any()))
@@ -229,6 +234,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
session.setAttribute(attrName, "attrValue");
Mono.just(session).subscribe(this.repository::save);
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());
@@ -242,6 +248,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
@Test
public void saveRemoveAttribute() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any()))
@@ -252,6 +259,7 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
session.removeAttribute(attrName);
Mono.just(session).subscribe(this.repository::save);
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
verify(this.redisOperations).expire(anyString(), any());

View File

@@ -16,6 +16,7 @@
package org.springframework.session.data.redis;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -522,14 +523,15 @@ public class RedisOperationsSessionRepositoryTests {
}
@Test
public void onMessageCreated() throws Exception {
public void onMessageCreated() {
MapSession session = this.cached;
byte[] pattern = "".getBytes("UTF-8");
String channel = "spring:session:event:created:" + session.getId();
byte[] pattern = "".getBytes(StandardCharsets.UTF_8);
String channel = "spring:session:event:0:created:" + session.getId();
JdkSerializationRedisSerializer defaultSerailizer = new JdkSerializationRedisSerializer();
this.redisRepository.setDefaultSerializer(defaultSerailizer);
byte[] body = defaultSerailizer.serialize(new HashMap());
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body);
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8), body);
this.redisRepository.setApplicationEventPublisher(this.publisher);
@@ -539,16 +541,16 @@ public class RedisOperationsSessionRepositoryTests {
assertThat(this.event.getValue().getSessionId()).isEqualTo(session.getId());
}
// gh-309
@Test
public void onMessageCreatedCustomSerializer() throws Exception {
@Test // gh-309
public void onMessageCreatedCustomSerializer() {
MapSession session = this.cached;
byte[] pattern = "".getBytes("UTF-8");
byte[] pattern = "".getBytes(StandardCharsets.UTF_8);
byte[] body = new byte[0];
String channel = "spring:session:event:created:" + session.getId();
String channel = "spring:session:event:0:created:" + session.getId();
given(this.defaultSerializer.deserialize(body))
.willReturn(new HashMap<String, Object>());
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body);
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8), body);
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, pattern);
@@ -559,7 +561,7 @@ public class RedisOperationsSessionRepositoryTests {
}
@Test
public void onMessageDeletedSessionFound() throws Exception {
public void onMessageDeletedSessionFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId)))
.willReturn(this.boundHashOperations);
@@ -570,10 +572,12 @@ public class RedisOperationsSessionRepositoryTests {
String channel = "__keyevent@0__:del";
String body = "spring:session:sessions:expires:" + deletedId;
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body.getBytes("UTF-8"));
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes("UTF-8"));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(deletedId)));
verify(this.boundHashOperations).entries();
@@ -586,7 +590,7 @@ public class RedisOperationsSessionRepositoryTests {
}
@Test
public void onMessageDeletedSessionNotFound() throws Exception {
public void onMessageDeletedSessionNotFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId)))
.willReturn(this.boundHashOperations);
@@ -594,10 +598,12 @@ public class RedisOperationsSessionRepositoryTests {
String channel = "__keyevent@0__:del";
String body = "spring:session:sessions:expires:" + deletedId;
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body.getBytes("UTF-8"));
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes("UTF-8"));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(deletedId)));
verify(this.boundHashOperations).entries();
@@ -608,7 +614,7 @@ public class RedisOperationsSessionRepositoryTests {
}
@Test
public void onMessageExpiredSessionFound() throws Exception {
public void onMessageExpiredSessionFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId)))
.willReturn(this.boundHashOperations);
@@ -619,10 +625,12 @@ public class RedisOperationsSessionRepositoryTests {
String channel = "__keyevent@0__:expired";
String body = "spring:session:sessions:expires:" + expiredId;
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body.getBytes("UTF-8"));
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes("UTF-8"));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(expiredId)));
verify(this.boundHashOperations).entries();
@@ -635,7 +643,7 @@ public class RedisOperationsSessionRepositoryTests {
}
@Test
public void onMessageExpiredSessionNotFound() throws Exception {
public void onMessageExpiredSessionNotFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId)))
.willReturn(this.boundHashOperations);
@@ -643,10 +651,12 @@ public class RedisOperationsSessionRepositoryTests {
String channel = "__keyevent@0__:expired";
String body = "spring:session:sessions:expires:" + expiredId;
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"), body.getBytes("UTF-8"));
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes("UTF-8"));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(expiredId)));
verify(this.boundHashOperations).entries();
@@ -881,6 +891,62 @@ public class RedisOperationsSessionRepositoryTests {
assertThat(session.getAttributeNames()).isEmpty();
}
@Test
public void onMessageCreatedInOtherDatabase() {
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.setDefaultSerializer(serializer);
MapSession session = this.cached;
String channel = "spring:session:event:created:1:" + session.getId();
byte[] body = serializer.serialize(new HashMap());
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8), body);
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
}
@Test
public void onMessageDeletedInOtherDatabase() {
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.setDefaultSerializer(serializer);
MapSession session = this.cached;
String channel = "__keyevent@1__:del";
String body = "spring:session:sessions:expires:" + session.getId();
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
}
@Test
public void onMessageExpiredInOtherDatabase() {
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.setDefaultSerializer(serializer);
MapSession session = this.cached;
String channel = "__keyevent@1__:expired";
String body = "spring:session:sessions:expires:" + session.getId();
DefaultMessage message = new DefaultMessage(
channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
assertThat(this.event.getAllValues()).isEmpty();
verifyZeroInteractions(this.publisher);
}
private String getKey(String id) {
return "spring:session:sessions:" + id;
}

View File

@@ -224,7 +224,7 @@ public abstract class AbstractHazelcastRepositoryITests {
assertThat(this.repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username))
.isNotNull();
.hasSize(1);
}
}

View File

@@ -1,7 +1,7 @@
<?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 hazelcast-config-3.9.xsd">
xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
<user-code-deployment enabled="true">
<class-cache-mode>ETERNAL</class-cache-mode>

View File

@@ -1,7 +1,7 @@
<?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 hazelcast-config-3.9.xsd">
xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
<group>
<name>spring-session-it-test-idle-time-map-name</name>

View File

@@ -1,7 +1,7 @@
<?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 hazelcast-config-3.9.xsd">
xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
<group>
<name>spring-session-it-test-map-name</name>

View File

@@ -29,10 +29,11 @@ import org.springframework.session.MapSession;
* Hazelcast {@link EntryProcessor} responsible for handling updates to session.
*
* @author Vedran Pavic
* @since 2.0.5
* @since 1.3.4
* @see HazelcastSessionRepository#save(HazelcastSessionRepository.HazelcastSession)
*/
class SessionUpdateEntryProcessor extends AbstractEntryProcessor<String, MapSession> {
public class SessionUpdateEntryProcessor
extends AbstractEntryProcessor<String, MapSession> {
private Instant lastAccessedTime;

View File

@@ -743,6 +743,46 @@ public abstract class AbstractJdbcOperationsSessionRepositoryITests {
assertThat(session.<String>getAttribute("testName")).isEqualTo("testValue2");
}
@Test // gh-1151
public void saveDeleted() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
this.repository.deleteById(session.getId());
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
assertThat(this.repository.findById(session.getId())).isNull();
}
@Test // gh-1151
public void saveDeletedAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
this.repository.deleteById(session.getId());
session.setLastAccessedTime(Instant.now());
session.setAttribute("testName", "testValue1");
this.repository.save(session);
assertThat(this.repository.findById(session.getId())).isNull();
}
@Test // gh-1203
public void saveWithLargeAttribute() {
String attributeName = "largeAttribute";
int arraySize = 4000;
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
session.setAttribute(attributeName, new byte[arraySize]);
this.repository.save(session);
session = this.repository.findById(session.getId());
assertThat(session).isNotNull();
assertThat((byte[]) session.getAttribute(attributeName)).hasSize(arraySize);
}
private String getSecurityName() {
return this.context.getAuthentication().getName();
}

View File

@@ -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.
@@ -86,7 +86,7 @@ public class MariaDb10JdbcOperationsSessionRepositoryITests
private static class MariaDb10Container extends MariaDBContainer<MariaDb10Container> {
MariaDb10Container() {
super("mariadb:10.3.8");
super("mariadb:10.3.12");
}
@Override

View File

@@ -86,7 +86,7 @@ public class MariaDb5JdbcOperationsSessionRepositoryITests
private static class MariaDb5Container extends MariaDBContainer<MariaDb5Container> {
MariaDb5Container() {
super("mariadb:5.5.60");
super("mariadb:5.5.62");
}
@Override

View File

@@ -85,7 +85,7 @@ public class MySql5JdbcOperationsSessionRepositoryITests
private static class MySql5Container extends MySQLContainer<MySql5Container> {
MySql5Container() {
super("mysql:5.7.22");
super("mysql:5.7.24");
}
@Override

View File

@@ -85,7 +85,7 @@ public class MySql8JdbcOperationsSessionRepositoryITests
private static class MySql8Container extends MySQLContainer<MySql8Container> {
MySql8Container() {
super("mysql:8.0.11");
super("mysql:8.0.13");
}
@Override

View File

@@ -86,7 +86,7 @@ public class PostgreSql10JdbcOperationsSessionRepositoryITests
extends PostgreSQLContainer<PostgreSql10Container> {
PostgreSql10Container() {
super("postgres:10.4");
super("postgres:10.6");
}
}

View File

@@ -86,7 +86,7 @@ public class PostgreSql9JdbcOperationsSessionRepositoryITests
extends PostgreSQLContainer<PostgreSql9Container> {
PostgreSql9Container() {
super("postgres:9.6.9");
super("postgres:9.6.11");
}
}

View File

@@ -86,9 +86,7 @@ public class SqlServerJdbcOperationsSessionRepositoryITests
extends MSSQLServerContainer<SqlServer2007Container> {
SqlServer2007Container() {
super("microsoft/mssql-server-linux:2017-CU8");
withStartupTimeoutSeconds(240);
withConnectTimeoutSeconds(240);
super("mcr.microsoft.com/mssql/server:2017-CU12");
}
}

View File

@@ -1 +1 @@
microsoft/mssql-server-linux:2017-CU8
mcr.microsoft.com/mssql/server:2017-CU12

View File

@@ -144,7 +144,9 @@ public class JdbcOperationsSessionRepository implements
private static final String CREATE_SESSION_ATTRIBUTE_QUERY =
"INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " +
"VALUES (?, ?, ?)";
"SELECT PRIMARY_ID, ?, ? " +
"FROM %TABLE_NAME% " +
"WHERE SESSION_ID = ?";
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 " +
@@ -381,9 +383,9 @@ public class JdbcOperationsSessionRepository implements
ps.setLong(6, session.getExpiryTime().toEpochMilli());
ps.setString(7, session.getPrincipalName());
});
if (!session.getAttributeNames().isEmpty()) {
final List<String> attributeNames = new ArrayList<>(session.getAttributeNames());
insertSessionAttributes(session, attributeNames);
Set<String> attributeNames = session.getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(session, new ArrayList<>(attributeNames));
}
}
@@ -410,17 +412,23 @@ public class JdbcOperationsSessionRepository implements
.filter((entry) -> entry.getValue() == DeltaValue.ADDED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
insertSessionAttributes(session, addedAttributeNames);
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(session, addedAttributeNames);
}
List<String> updatedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
updateSessionAttributes(session, updatedAttributeNames);
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(session, updatedAttributeNames);
}
List<String> removedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
deleteSessionAttributes(session, removedAttributeNames);
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(session, removedAttributeNames);
}
}
});
@@ -490,18 +498,16 @@ public class JdbcOperationsSessionRepository implements
}
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
if (attributeNames == null || attributeNames.isEmpty()) {
return;
}
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, session.primaryKey);
ps.setString(2, attributeName);
serialize(ps, 3, session.getAttribute(attributeName));
ps.setString(1, attributeName);
serialize(ps, 2, session.getAttribute(attributeName));
ps.setString(3, session.getId());
}
@Override
@@ -514,17 +520,15 @@ public class JdbcOperationsSessionRepository implements
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
serialize(ps, 3, session.getAttribute(attributeName));
ps.setString(1, attributeName);
serialize(ps, 2, session.getAttribute(attributeName));
ps.setString(3, session.getId());
});
}
}
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
if (attributeNames == null || attributeNames.isEmpty()) {
return;
}
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
@@ -554,9 +558,7 @@ public class JdbcOperationsSessionRepository implements
}
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
if (attributeNames == null || attributeNames.isEmpty()) {
return;
}
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
@@ -739,23 +741,23 @@ public class JdbcOperationsSessionRepository implements
}
if (attributeExists) {
if (attributeRemoved) {
this.delta.merge(attributeName, DeltaValue.REMOVED,
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED
? null
: deltaValue));
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));
(oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
? oldDeltaValue
: deltaValue);
}
}
else {
this.delta.merge(attributeName, DeltaValue.ADDED,
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
? oldDeltaValue
: DeltaValue.UPDATED));
: DeltaValue.UPDATED);
}
this.delegate.setAttribute(attributeName, attributeValue);
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) ||

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2017 the original author or authors.
* Copyright 2014-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,9 @@ import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
@@ -102,6 +105,11 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
}
else if (requiresTemporaryLob(this.dataSource)) {
DefaultLobHandler lobHandler = new DefaultLobHandler();
lobHandler.setCreateTemporaryLob(true);
sessionRepository.setLobHandler(lobHandler);
}
if (this.springSessionConversionService != null) {
sessionRepository.setConversionService(this.springSessionConversionService);
}
@@ -115,6 +123,17 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
return sessionRepository;
}
private static boolean requiresTemporaryLob(DataSource dataSource) {
try {
String productName = JdbcUtils.extractDatabaseMetaData(dataSource,
"getDatabaseProductName");
return "Oracle".equalsIgnoreCase(JdbcUtils.commonDatabaseName(productName));
}
catch (MetaDataAccessException ex) {
return false;
}
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}