Compare commits
96 Commits
2.1.6.RELE
...
2.0.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce714708cb | ||
|
|
748d14e04a | ||
|
|
b4cbb56ef2 | ||
|
|
b94d3a0dd0 | ||
|
|
8abcd9cc65 | ||
|
|
207b69527b | ||
|
|
dd12125085 | ||
|
|
3af1327474 | ||
|
|
e296702da1 | ||
|
|
c6572e9679 | ||
|
|
83569af5a1 | ||
|
|
d240e5ead0 | ||
|
|
1d5b4149ae | ||
|
|
8e2b51ffc6 | ||
|
|
48a83870e7 | ||
|
|
6cec5dd548 | ||
|
|
55a70fa5be | ||
|
|
61bea92e83 | ||
|
|
01c49c145b | ||
|
|
7c8fe93f2f | ||
|
|
144f4be830 | ||
|
|
357b4c8394 | ||
|
|
a28bbdc997 | ||
|
|
d6dc0f5cdd | ||
|
|
de521cd07c | ||
|
|
4662d5c303 | ||
|
|
53de486da3 | ||
|
|
d11e6ddfe9 | ||
|
|
6ed798e09f | ||
|
|
adcfa87454 | ||
|
|
c66fcf3f8b | ||
|
|
ae6bc3e550 | ||
|
|
44f63cd301 | ||
|
|
0a3dbfa43e | ||
|
|
ae77a9db6c | ||
|
|
fe106ea7bb | ||
|
|
44ba9a97b7 | ||
|
|
0bdb106c30 | ||
|
|
43014247eb | ||
|
|
7e8917ac47 | ||
|
|
ad90867590 | ||
|
|
32c28013f3 | ||
|
|
c5b43f096c | ||
|
|
82759642c3 | ||
|
|
74c5260754 | ||
|
|
55b4f6f017 | ||
|
|
9099bd5d3a | ||
|
|
afa1f0890e | ||
|
|
ed3f6abf5d | ||
|
|
6c322631d4 | ||
|
|
9575be9b7d | ||
|
|
eae239febf | ||
|
|
b86b34ca2e | ||
|
|
c0a2220d3b | ||
|
|
c9d6ef7f01 | ||
|
|
c2c1311830 | ||
|
|
8c97a73b36 | ||
|
|
0886e237b6 | ||
|
|
c57a286e35 | ||
|
|
4c5f22900d | ||
|
|
8a8f379b37 | ||
|
|
dc4a0ce61b | ||
|
|
5944648c25 | ||
|
|
4502724e8c | ||
|
|
3c8cce652e | ||
|
|
425df2261f | ||
|
|
7c6b143964 | ||
|
|
39c640f456 | ||
|
|
f190da3757 | ||
|
|
34060cf0f6 | ||
|
|
ff10709f18 | ||
|
|
6e471f6441 | ||
|
|
a03be43450 | ||
|
|
7e8f500df0 | ||
|
|
b27742ce3e | ||
|
|
af44e71af0 | ||
|
|
055c2bcb93 | ||
|
|
3335deb5d5 | ||
|
|
1079e9e016 | ||
|
|
5fa52be8d1 | ||
|
|
c90952031f | ||
|
|
ce308ca513 | ||
|
|
c8f78e510e | ||
|
|
748ba70a01 | ||
|
|
dc1c7cdf02 | ||
|
|
3a972bef76 | ||
|
|
6b7fc3af08 | ||
|
|
535160bc92 | ||
|
|
606e08007e | ||
|
|
06fa33e48b | ||
|
|
d7c2e8e79c | ||
|
|
6bec95a298 | ||
|
|
9249a140c9 | ||
|
|
7f6dc801e0 | ||
|
|
83d46ad685 | ||
|
|
21cef2b7fa |
149
Jenkinsfile
vendored
149
Jenkinsfile
vendored
@@ -1,149 +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('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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk9: {
|
||||
stage('JDK 9') {
|
||||
timeout(time: 45, unit: 'MINUTES') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${tool 'jdk9'}"]) {
|
||||
sh './gradlew clean test --no-daemon --refresh-dependencies'
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
currentBuild.result = 'FAILED: jdk9'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk10: {
|
||||
stage('JDK 10') {
|
||||
timeout(time: 45, unit: 'MINUTES') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${tool 'jdk10'}"]) {
|
||||
sh './gradlew clean test --no-daemon --refresh-dependencies'
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
currentBuild.result = 'FAILED: jdk10'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk11: {
|
||||
stage('JDK 11') {
|
||||
timeout(time: 45, unit: 'MINUTES') {
|
||||
node('ubuntu1804') {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${tool 'jdk11'}"]) {
|
||||
sh './gradlew clean test integrationTest --no-daemon --refresh-dependencies'
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
currentBuild.result = 'FAILED: jdk11'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBuild.result == 'SUCCESS') {
|
||||
parallel artifacts: {
|
||||
stage('Deploy Artifacts') {
|
||||
node {
|
||||
checkout scm
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
docs: {
|
||||
stage('Deploy Docs') {
|
||||
node {
|
||||
checkout scm
|
||||
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 {
|
||||
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"]
|
||||
|
||||
34
build.gradle
34
build.gradle
@@ -1,38 +1,18 @@
|
||||
buildscript {
|
||||
ext {
|
||||
releaseBuild = version.endsWith('RELEASE')
|
||||
snapshotBuild = version.endsWith('SNAPSHOT')
|
||||
milestoneBuild = !(releaseBuild || snapshotBuild)
|
||||
|
||||
springBootVersion = '2.1.4.RELEASE'
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://repo.spring.io/plugins-release/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.25.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://repo.spring.io/plugins-release' }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
|
||||
group = 'org.springframework.session'
|
||||
description = 'Spring Session'
|
||||
|
||||
gradle.taskGraph.whenReady { graph ->
|
||||
def jacocoEnabled = graph.allTasks.any { it instanceof JacocoReport }
|
||||
subprojects {
|
||||
plugins.withType(JavaPlugin) {
|
||||
sourceCompatibility = 1.8
|
||||
}
|
||||
plugins.withType(JacocoPlugin) {
|
||||
tasks.withType(Test) {
|
||||
jacoco.enabled = jacocoEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ext.releaseBuild = version.endsWith('RELEASE')
|
||||
ext.snapshotBuild = version.endsWith('SNAPSHOT')
|
||||
ext.milestoneBuild = !(releaseBuild || snapshotBuild)
|
||||
|
||||
@@ -4,141 +4,137 @@ Rob Winch
|
||||
|
||||
This guide describes how to use Spring Session to find sessions by username.
|
||||
|
||||
NOTE: You can find the completed guide in the <<findbyusername-sample, findbyusername application>>.
|
||||
NOTE: The completed guide can be found in the <<findbyusername-sample, findbyusername application>>.
|
||||
|
||||
|
||||
[[findbyusername-assumptions]]
|
||||
== Assumptions
|
||||
|
||||
The guide assumes you have already added Spring Session to your application by using the built-in Redis configuration support.
|
||||
The guide assumes you have already added Spring Session using the built in Redis configuration support to your application.
|
||||
The guide also assumes you have already applied Spring Security to your application.
|
||||
However, we the guide is somewhat general purpose and can be applied to any technology with minimal changes, which we discuss later in the guide.
|
||||
However, we the guide will be somewhat general purpose and can be applied to any technology with minimal changes we will discuss.
|
||||
|
||||
NOTE: If you need to learn how to add Spring Session to your project, see the listing of link:../#samples[samples and guides]
|
||||
[NOTE]
|
||||
====
|
||||
If you need to learn how to add Spring Session to your project, please refer to the listing of link:../#samples[samples and guides]
|
||||
====
|
||||
|
||||
== About the Sample
|
||||
|
||||
Our sample uses this feature to invalidate the users session that might have been compromised.
|
||||
Our sample is using this feature to invalidate the users session that might have been compromised.
|
||||
Consider the following scenario:
|
||||
|
||||
* User goes to library and authenticates to the application.
|
||||
* User goes home and realizes they forgot to log out.
|
||||
* User can log in and terminate the session from the library using clues like the location, created time, last accessed time, and so on.
|
||||
* User goes to library and authenticates to the application
|
||||
* User goes home and realizes they forgot to log out
|
||||
* User can log in and terminate the session from the library using clues like the location, created time, last accessed time, etc.
|
||||
|
||||
Would it not be nice if we could let the user invalidate the session at the library from any device with which they authenticate?
|
||||
Wouldn't it be nice if we could allow the user to invalidate the session at the library from any device they authenticate with?
|
||||
This sample demonstrates how this is possible.
|
||||
|
||||
[[findbyindexnamesessionrepository]]
|
||||
== Using `FindByIndexNameSessionRepository`
|
||||
== FindByIndexNameSessionRepository
|
||||
|
||||
To look up a user by their username, you must first choose a `SessionRepository` that implements link:../#api-findbyindexnamesessionrepository[`FindByIndexNameSessionRepository`].
|
||||
Our sample application assumes that the Redis support is already set up, so we are ready to go.
|
||||
In order to look up a user by their username, you must first choose a `SessionRepository` that implements link:../#api-findbyindexnamesessionrepository[FindByIndexNameSessionRepository].
|
||||
Our sample application assumes that the Redis support is already setup, so we are ready to go.
|
||||
|
||||
== Mapping the User Name
|
||||
== Mapping the username
|
||||
|
||||
`FindByIndexNameSessionRepository` can find a session only by the user name if the developer instructs Spring Session what user is associated with the `Session`.
|
||||
You can do so by ensuring that the session attribute with the name `FindByUsernameSessionRepository.PRINCIPAL_NAME_INDEX_NAME` is populated with the username.
|
||||
`FindByIndexNameSessionRepository` can only find a session by the username, if the developer instructs Spring Session what user is associated with the `Session`.
|
||||
This is done by ensuring that the session attribute with the name `FindByUsernameSessionRepository.PRINCIPAL_NAME_INDEX_NAME` is populated with the username.
|
||||
|
||||
Generally speaking, you can do so with the following code immediately after the user authenticates:
|
||||
Generally, speaking this can be done with the following code immediately after the user authenticates:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{docs-test-dir}docs/FindByIndexNameSessionRepositoryTests.java[tags=set-username]
|
||||
----
|
||||
====
|
||||
|
||||
== Mapping the User Name with Spring Security
|
||||
== Mapping the username with Spring Security
|
||||
|
||||
Since we use Spring Security, the user name is automatically indexed for us.
|
||||
This means we need not perform any steps to ensure the user name is indexed.
|
||||
Since we are using Spring Security, the user name is automatically indexed for us.
|
||||
This means we will not have to perform any steps to ensure the user name is indexed.
|
||||
|
||||
== Adding Additional Data to the Session
|
||||
== Adding Additional Data to Session
|
||||
|
||||
It may be nice to associate additional information (such as the IP Address, the browser, location, and other details) to the session.
|
||||
Doing so makes it easier for the user to know which session they are looking at.
|
||||
It may be nice to associate additional information (i.e. IP Address, the browser, location, etc) to the session.
|
||||
This makes it easier for the user to know which session they are looking at.
|
||||
|
||||
To do so, determine which session attribute you want to use and what information you wish to provide.
|
||||
To do this simply determine which session attribute you want to use and what information you wish to provide.
|
||||
Then create a Java bean that is added as a session attribute.
|
||||
For example, our sample application includes the location and access type of the session, as the following listing shows:
|
||||
For example, our sample application includes the location and access type of the session
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}boot/findbyusername/src/main/java/sample/session/SessionDetails.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
We then inject that information into the session on each HTTP request using a `SessionDetailsFilter`, as the following example shows:
|
||||
We then inject that information into the session on each HTTP request using a `SessionDetailsFilter`.
|
||||
For example:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}boot/findbyusername/src/main/java/sample/session/SessionDetailsFilter.java[tags=dofilterinternal]
|
||||
----
|
||||
====
|
||||
|
||||
We obtain the information we want and then set the `SessionDetails` as an attribute in the `Session`.
|
||||
When we retrieve the `Session` by user name, we can then use the session to access our `SessionDetails` as we would any other session attribute.
|
||||
When we retrieve the `Session` by username, we can then use the session to access our `SessionDetails` just like any other session attribute.
|
||||
|
||||
NOTE: You might wonder why Spring Session does not provide `SessionDetails` functionality out of the box.
|
||||
We have two reasons.
|
||||
The first reason is that it is very trivial for applications to implement this themselves.
|
||||
The second reason is that the information that is populated in the session (and how frequently that information is updated) is highly application-dependent.
|
||||
[NOTE]
|
||||
====
|
||||
You might be wondering at this point why Spring Session does not provide `SessionDetails` functionality out of the box.
|
||||
The reason, is twofold.
|
||||
The first is that it is very trivial for applications to implement this themselves.
|
||||
The second reason is that the information that is populated in the session (and how frequently that information is updated) is highly application dependent.
|
||||
====
|
||||
|
||||
== Finding sessions for a specific user
|
||||
|
||||
We can now find all the sessions for a specific user.
|
||||
The following example shows how to do so:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}boot/findbyusername/src/main/java/sample/mvc/IndexController.java[tags=findbyusername]
|
||||
----
|
||||
====
|
||||
|
||||
In our instance, we find all sessions for the currently logged in user.
|
||||
However, you can modify this for an administrator to use a form to specify which user to look up.
|
||||
However, this could easily be modified for an administrator to use a form to specify which user to look up.
|
||||
|
||||
[[findbyusername-sample]]
|
||||
== `findbyusername` Sample Application
|
||||
== findbyusername Sample Application
|
||||
|
||||
This section describes how to use the `findbyusername` sample application.
|
||||
|
||||
=== Running the `findbyusername` Sample Application
|
||||
=== Running the findbyusername Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-findbyusername:bootRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
You can now try using the application. Enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
You should also see a listing of active sessions for the currently logged in user.
|
||||
|
||||
You can emulate the flow we discussed in the <<About the Sample>> section by doing the following:
|
||||
Let's emulate the flow we discussed in the <<About the Sample>> section
|
||||
|
||||
* Open a new incognito window and navigate to http://localhost:8080/
|
||||
* Enter the following to log in:
|
||||
** *Username* _user_
|
||||
** *Password* _password_
|
||||
* Terminate your original session.
|
||||
* Refresh the original window and see that you are logged out.
|
||||
** **Username** _user_
|
||||
** **Password** _password_
|
||||
* Terminate your original session
|
||||
* Refresh the original window and see you are logged out
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` when you use Spring Boot.
|
||||
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` when using Spring Boot.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-boot-sample, httpsession-jdbc-boot sample application>>.
|
||||
NOTE: The completed guide can be found in the <<httpsession-jdbc-boot-sample, httpsession-jdbc-boot sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
We assume you are working with a working Spring Boot web application.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -26,9 +24,8 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
Spring Boot provides dependency management for Spring Session modules, so you need not explicitly declare the dependency version.
|
||||
Spring Boot provides dependency management for Spring Session modules, so there's no need to explicitly declare dependency version.
|
||||
|
||||
// tag::config[]
|
||||
|
||||
@@ -36,106 +33,95 @@ Spring Boot provides dependency management for Spring Session modules, so you ne
|
||||
== Spring Boot Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring Boot configuration.
|
||||
Thanks to first-class auto configuration support, setting up Spring Session backed by a relational database is as simple as adding a single configuration property to your `application.properties`.
|
||||
The following listing shows how to do so:
|
||||
Thanks to first-class auto configuration support, setting up Spring Session backed by a relational database is as simple as adding a single configuration property to your `application.properties`:
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
spring.session.store-type=jdbc # Session store type.
|
||||
----
|
||||
====
|
||||
|
||||
Under the hood, Spring Boot applies configuration that is equivalent to manually adding the `@EnableJdbcHttpSession` annotation.
|
||||
This creates a Spring bean with the name of `springSessionRepositoryFilter`. That bean implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
Under the hood, Spring Boot will apply configuration that is equivalent to manually adding `@EnableJdbcHttpSession` annotation.
|
||||
This creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
|
||||
You can further customize by using `application.properties`.
|
||||
The following listing shows how to do so:
|
||||
Further customization is possible using `application.properties`:
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds are used.
|
||||
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds will be used.
|
||||
spring.session.jdbc.initialize-schema=embedded # Database schema initialization mode.
|
||||
spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema.
|
||||
spring.session.jdbc.table-name=SPRING_SESSION # Name of the database table used to store sessions.
|
||||
----
|
||||
====
|
||||
|
||||
For more information, see the https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-session[Spring Session] portion of the Spring Boot documentation.
|
||||
For more information, refer to https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-session[Spring Session] portion of the Spring Boot documentation.
|
||||
|
||||
[[httpsession-jdbc-boot-configuration]]
|
||||
== Configuring the `DataSource`
|
||||
== Configuring the DataSource
|
||||
|
||||
Spring Boot automatically creates a `DataSource` that connects Spring Session to an embedded instance of an H2 database.
|
||||
In a production environment, you need to update your configuration to point to your relational database.
|
||||
For example, you can include the following in your application.properties:
|
||||
Spring Boot automatically creates a `DataSource` that connects Spring Session to an embedded instance of H2 database.
|
||||
In a production environment you need to ensure to update your configuration to point to your relational database.
|
||||
For example, you can include the following in your *application.properties*
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
spring.datasource.url= # JDBC URL of the database.
|
||||
spring.datasource.username= # Login username of the database.
|
||||
spring.datasource.password= # Login password of the database.
|
||||
----
|
||||
====
|
||||
|
||||
For more information, see the https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-configure-datasource[Configure a DataSource] portion of the Spring Boot documentation.
|
||||
For more information, refer to https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-configure-datasource[Configure a DataSource] portion of the Spring Boot documentation.
|
||||
|
||||
[[httpsession-jdbc-boot-servlet-configuration]]
|
||||
== Servlet Container Initialization
|
||||
|
||||
Our <<httpsession-jdbc-boot-spring-configuration,Spring Boot Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<httpsession-jdbc-boot-spring-configuration,Spring Boot Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Boot takes care of both of these steps for us.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[httpsession-jdbc-boot-sample]]
|
||||
== `httpsession-jdbc-boot` Sample Application
|
||||
== httpsession-jdbc-boot Sample Application
|
||||
|
||||
The httpsession-jdbc-boot Sample Application demonstrates how to use Spring Session to transparently leverage an H2 database to back a web application's `HttpSession` when you use Spring Boot.
|
||||
The httpsession-jdbc-boot Sample Application demonstrates how to use Spring Session to transparently leverage H2 database to back a web application's `HttpSession` when using Spring Boot.
|
||||
|
||||
[[httpsession-jdbc-boot-running]]
|
||||
=== Running the `httpsession-jdbc-boot` Sample Application
|
||||
=== Running the httpsession-jdbc-boot Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-jdbc:bootRun
|
||||
----
|
||||
====
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
[[httpsession-jdbc-boot-explore]]
|
||||
=== Exploring the Security Sample Application
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
You can now try using the application.
|
||||
To do so, enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
You should now see a message indicating that your are logged in with the user entered previously.
|
||||
The user's information is stored in the H2 database rather than Tomcat's `HttpSession` implementation.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
The user's information is stored in H2 database rather than Tomcat's `HttpSession` implementation.
|
||||
|
||||
[[httpsession-jdbc-boot-how]]
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in the H2 database.
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in H2 database.
|
||||
Spring Session replaces the `HttpSession` with an implementation that is backed by a relational database.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`, it is then persisted into the H2 database.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into H2 database.
|
||||
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named `SESSION` in your browser. That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session by using the H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL).
|
||||
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and see that we are no longer authenticated.
|
||||
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
Rob Winch, Vedran Pavić
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use Spring Boot.
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Spring Boot.
|
||||
|
||||
NOTE: You can find the completed guide in the <<boot-sample, boot sample application>>.
|
||||
NOTE: The completed guide can be found in the <<boot-sample, boot sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must ensure your dependencies.
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
We assume you are working with a working Spring Boot web application.
|
||||
If you are using Maven, you must add the following dependencies:
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -26,131 +24,114 @@ If you are using Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
Spring Boot provides dependency management for Spring Session modules, so you need not explicitly declare dependency version.
|
||||
Spring Boot provides dependency management for Spring Session modules, so there's no need to explicitly declare dependency version.
|
||||
|
||||
[[boot-spring-configuration]]
|
||||
== Spring Boot Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring Boot configuration.
|
||||
Thanks to first-class auto configuration support, setting up Spring Session backed by Redis is as simple as adding a single configuration property to your `application.properties`, as the following listing shows:
|
||||
Thanks to first-class auto configuration support, setting up Spring Session backed by Redis is as simple as adding a single configuration property to your `application.properties`:
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
spring.session.store-type=redis # Session store type.
|
||||
----
|
||||
====
|
||||
|
||||
Under the hood, Spring Boot applies configuration that is equivalent to manually adding `@EnableRedisHttpSession` annotation.
|
||||
This creates a Spring bean with the name of `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
Under the hood, Spring Boot will apply configuration that is equivalent to manually adding `@EnableRedisHttpSession` annotation.
|
||||
This creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
|
||||
Further customization is possible by using `application.properties`, as the following listing shows:
|
||||
Further customization is possible using `application.properties`:
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
|
||||
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds will be used.
|
||||
spring.session.redis.flush-mode=on-save # Sessions flush mode.
|
||||
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
|
||||
----
|
||||
====
|
||||
|
||||
For more information, see the https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-session[Spring Session] portion of the Spring Boot documentation.
|
||||
For more information, refer to https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-session[Spring Session] portion of the Spring Boot documentation.
|
||||
|
||||
[[boot-redis-configuration]]
|
||||
== Configuring the Redis Connection
|
||||
|
||||
Spring Boot automatically creates a `RedisConnectionFactory` that connects Spring Session to a Redis Server on localhost on port 6379 (default port).
|
||||
In a production environment, you need to update your configuration to point to your Redis server.
|
||||
For example, you can include the following in your application.properties:
|
||||
In a production environment you need to ensure to update your configuration to point to your Redis server.
|
||||
For example, you can include the following in your *application.properties*
|
||||
|
||||
====
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
spring.redis.host=localhost # Redis server host.
|
||||
spring.redis.password= # Login password of the redis server.
|
||||
spring.redis.port=6379 # Redis server port.
|
||||
----
|
||||
====
|
||||
|
||||
For more information, see the https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation.
|
||||
For more information, refer to https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation.
|
||||
|
||||
[[boot-servlet-configuration]]
|
||||
== Servlet Container Initialization
|
||||
|
||||
Our <<boot-spring-configuration,Spring Boot Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<boot-spring-configuration,Spring Boot Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
Last, we need to ensure that our servlet container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Boot takes care of both of these steps for us.
|
||||
|
||||
[[boot-sample]]
|
||||
== Boot Sample Application
|
||||
|
||||
The Boot Sample Application demonstrates how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use Spring Boot.
|
||||
The Boot Sample Application demonstrates how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Spring Boot.
|
||||
|
||||
[[boot-running]]
|
||||
=== Running the Boot Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-redis:bootRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
[[boot-explore]]
|
||||
=== Exploring the `security` Sample Application
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
Now you can try using the application. Enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
The user's information is stored in Redis rather than Tomcat's `HttpSession` implementation.
|
||||
|
||||
[[boot-how]]
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Redis.
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
|
||||
Spring Session replaces the `HttpSession` with an implementation that is backed by Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`, it is then persisted into Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Redis.
|
||||
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named `SESSION` in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session by using redis-cli.
|
||||
For example, on a Linux based system you can type the following:
|
||||
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key.
|
||||
To do so, enter the following into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your `SESSION` cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
=====
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
|
||||
@@ -7,8 +7,9 @@ This guide describes how to use Spring Session to ensure that WebSocket messages
|
||||
|
||||
// tag::disclaimer[]
|
||||
|
||||
NOTE: Spring Session's WebSocket support works only with Spring's WebSocket support.
|
||||
Specifically,it does not work with using https://www.jcp.org/en/jsr/detail?id=356[JSR-356] directly, because JSR-356 does not have a mechanism for intercepting incoming WebSocket messages.
|
||||
NOTE: Spring Session's WebSocket support only works with Spring's WebSocket support.
|
||||
Specifically it does not work with using https://www.jcp.org/en/jsr/detail?id=356[JSR-356] directly.
|
||||
This is due to the fact that JSR-356 does not have a mechanism for intercepting incoming WebSocket messages.
|
||||
|
||||
// end::disclaimer[]
|
||||
|
||||
@@ -16,27 +17,24 @@ Specifically,it does not work with using https://www.jcp.org/en/jsr/detail?id=35
|
||||
|
||||
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:httpsession.html[HttpSession Guide].
|
||||
|
||||
Please make sure you have already integrated Spring Session with HttpSession before proceeding.
|
||||
Please make sure you have already integrated Spring Session with the HttpSession before proceeding.
|
||||
|
||||
// tag::config[]
|
||||
|
||||
[[websocket-spring-configuration]]
|
||||
== Spring Configuration
|
||||
|
||||
In a typical Spring WebSocket application, you would implement `WebSocketMessageBrokerConfigurer`.
|
||||
In a typical Spring WebSocket application users would implement `WebSocketMessageBrokerConfigurer`.
|
||||
For example, the configuration might look something like the following:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{websocketdoc-test-dir}WebSocketConfig.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
We can update our configuration to use Spring Session's WebSocket support.
|
||||
The following example shows how to do so:
|
||||
We can easily update our configuration to use Spring Session's WebSocket support.
|
||||
For example:
|
||||
|
||||
====
|
||||
.src/main/java/samples/config/WebSocketConfig.java
|
||||
[source,java]
|
||||
----
|
||||
@@ -45,9 +43,8 @@ include::{samples-dir}boot/websocket/src/main/java/sample/config/WebSocketConfig
|
||||
|
||||
To hook in the Spring Session support we only need to change two things:
|
||||
|
||||
<1> Instead of implementing `WebSocketMessageBrokerConfigurer`, we extend `AbstractSessionWebSocketMessageBrokerConfigurer`
|
||||
<1> Instead of implementing `WebSocketMessageBrokerConfigurer` we extend `AbstractSessionWebSocketMessageBrokerConfigurer`
|
||||
<2> We rename the `registerStompEndpoints` method to `configureStompEndpoints`
|
||||
====
|
||||
|
||||
What does `AbstractSessionWebSocketMessageBrokerConfigurer` do behind the scenes?
|
||||
|
||||
@@ -55,89 +52,87 @@ What does `AbstractSessionWebSocketMessageBrokerConfigurer` do behind the scenes
|
||||
This ensures a custom `SessionConnectEvent` is fired that contains the `WebSocketSession`.
|
||||
The `WebSocketSession` is necessary to terminate any WebSocket connections that are still open when a Spring Session is terminated.
|
||||
* `SessionRepositoryMessageInterceptor` is added as a `HandshakeInterceptor` to every `StompWebSocketEndpointRegistration`.
|
||||
This ensures that the `Session` is added to the WebSocket properties to enable updating the last accessed time.
|
||||
This ensures that the Session is added to the WebSocket properties to enable updating the last accessed time.
|
||||
* `SessionRepositoryMessageInterceptor` is added as a `ChannelInterceptor` to our inbound `ChannelRegistration`.
|
||||
This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.
|
||||
* `WebSocketRegistryListener` is created as a Spring bean.
|
||||
This ensures that we have a mapping of all of the `Session` IDs to the corresponding WebSocket connections.
|
||||
* `WebSocketRegistryListener` is created as a Spring Bean.
|
||||
This ensures that we have a mapping of all of the Session id to the corresponding WebSocket connections.
|
||||
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is terminated.
|
||||
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[websocket-sample]]
|
||||
== `websocket` Sample Application
|
||||
== websocket Sample Application
|
||||
|
||||
The `websocket` sample application demonstrates how to use Spring Session with WebSockets.
|
||||
The websocket sample application demonstrates how to use Spring Session with WebSockets.
|
||||
|
||||
=== Running the `websocket` Sample Application
|
||||
=== Running the websocket Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-websocket:bootRun
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
=====
|
||||
For the purposes of testing session expiration, you may want to change the session expiration to be 1 minute (the default is 30 minutes) by adding the following configuration property before starting the application:
|
||||
|
||||
====
|
||||
For the purposes of testing session expiration, you may want to change the session expiration to be 1 minute (default is 30 minutes) by adding the following configuration property starting before the application:
|
||||
|
||||
.src/main/resources/application.properties
|
||||
----
|
||||
server.servlet.session.timeout=1m # Session timeout. If a duration suffix is not specified, seconds will be used.
|
||||
----
|
||||
====
|
||||
=====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-boot-websocket:bootRun
|
||||
----
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `websocket` Sample Application
|
||||
=== Exploring the websocket Sample Application
|
||||
|
||||
Now you can try using the application. Authenticate with the following information:
|
||||
Try using the application. Authenticate with the following information:
|
||||
|
||||
* *Username* _rob_
|
||||
* *Password* _password_
|
||||
* **Username** _rob_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button. You should now be authenticated as the user **rob**.
|
||||
Now click the **Login** button. You should now be authenticated as the user **rob**.
|
||||
|
||||
Open an incognito window and access http://localhost:8080/
|
||||
|
||||
You are prompted with a login form. Authenticate with the following information:
|
||||
You will be prompted with a log in form. Authenticate with the following information:
|
||||
|
||||
* *Username* _luke_
|
||||
* *Password* _password_
|
||||
* **Username** _luke_
|
||||
* **Password** _password_
|
||||
|
||||
Now send a message from rob to luke. The message should appear.
|
||||
Now send a message from *rob* to *luke*. The message should appear.
|
||||
|
||||
Wait for two minutes and try sending a message from rob to luke again.
|
||||
You can see that the message is no longer sent.
|
||||
Wait for two minutes and try sending a message from *rob* to *luke* again.
|
||||
You will see that the message is no longer sent.
|
||||
|
||||
[NOTE]
|
||||
.Why two minutes?
|
||||
====
|
||||
Spring Session expires in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds.
|
||||
Spring Session will expire in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds.
|
||||
To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions.
|
||||
This means you need to wait at most two minutes before the WebSocket connection is terminated.
|
||||
This means you will need to wait at most two minutes before the WebSocket connection is terminated.
|
||||
====
|
||||
|
||||
You can now try accessing http://localhost:8080/
|
||||
You are prompted to authenticate again.
|
||||
Try accessing http://localhost:8080/
|
||||
You will be prompted to authenticate again.
|
||||
This demonstrates that the session properly expires.
|
||||
|
||||
Now repeat the same exercise, but instead of waiting two minutes, send a message from each of the users every 30 seconds.
|
||||
You can see that the messages continue to be sent.
|
||||
Now repeat the same exercise, but instead of waiting two minutes send a message from *each* of the users every 30 seconds.
|
||||
You will see that the messages continue to be sent.
|
||||
Try accessing http://localhost:8080/
|
||||
You are not prompted to authenticate again.
|
||||
You will not be prompted to authenticate again.
|
||||
This demonstrates the session is kept alive.
|
||||
|
||||
NOTE: Only messages sent from a user keep the session alive.
|
||||
This is because only messages coming from a user imply user activity.
|
||||
Received messages do not imply activity and, thus, do not renew the session expiration.
|
||||
Messages received do not imply activity and thus do not renew the session expiration.
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
Eric Helgeson
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use Grails 3.1
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Grails 3.1
|
||||
|
||||
NOTE: Grails 3.1 is based off spring boot 1.3, so much of the advanced configuration and options can be found in the Boot docs as well.
|
||||
NOTE: Grails 3.1 is based off spring boot 1.3 so much of the advanced configuration and options can be found in the boot docs as well.
|
||||
|
||||
NOTE: You can find the completed guid in the <<grails3-sample, Grails 3 sample application>>.
|
||||
NOTE: The completed guide can be found in the <<grails3-sample, Grails 3 sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
We assume you are working with a working Grails 3.1 web profile.
|
||||
You must add the following dependencies:
|
||||
Add the following dependencies:
|
||||
|
||||
====
|
||||
.build.gradle
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -24,13 +22,11 @@ dependencies {
|
||||
compile 'org.springframework.session:spring-session:{spring-session-version}'
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we use a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your build.gradle:
|
||||
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.build.gradle
|
||||
[source,groovy]
|
||||
----
|
||||
@@ -40,14 +36,12 @@ repositories {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we use a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your build.gradle:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.build.gradle
|
||||
[source,groovy]
|
||||
----
|
||||
@@ -57,7 +51,6 @@ repositories {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
[[grails3-redis-configuration]]
|
||||
@@ -65,9 +58,8 @@ endif::[]
|
||||
|
||||
Spring Boot automatically creates a `RedisConnectionFactory` that connects Spring Session to a Redis Server on localhost on port 6379 (default port).
|
||||
In a production environment you need to ensure to update your configuration to point to your Redis server.
|
||||
For example, you can include the following in your application.yml:
|
||||
For example, you can include the following in your *application.yml*
|
||||
|
||||
====
|
||||
.grails-app/conf/application.yml
|
||||
[source,yml]
|
||||
----
|
||||
@@ -77,9 +69,8 @@ spring:
|
||||
password: secret
|
||||
port: 6397
|
||||
----
|
||||
====
|
||||
|
||||
For more information, see the https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation.
|
||||
For more information, refer to https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation.
|
||||
|
||||
[[grails3-sample]]
|
||||
== Grails 3 Sample Application
|
||||
@@ -91,61 +82,52 @@ The Grails 3 Sample Application demonstrates how to use Spring Session to transp
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-misc-grails3:bootRun
|
||||
----
|
||||
|
||||
NOTE:For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/test/index
|
||||
|
||||
[[grails3-explore]]
|
||||
=== Exploring the `security` Sample Application
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
You can now try using the application. Enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
You should now see a message indicating that your are logged in with the user entered previously.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
The user's information is stored in Redis rather than Tomcat's `HttpSession` implementation.
|
||||
|
||||
[[grails3-how]]
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Redis.
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
|
||||
Spring Session replaces the `HttpSession` with an implementation that is backed by Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`, it is then persisted into Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Redis.
|
||||
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named `SESSION` in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session by using redis-cli.
|
||||
For example, on a Linux based system you can type the following:
|
||||
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key.
|
||||
To do so, enter the following into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your `SESSION` cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
Now you can visit the application at http://localhost:8080/test/index and see that we are no longer authenticated.
|
||||
Now visit the application at http://localhost:8080/test/index and observe that we are no longer authenticated.
|
||||
|
||||
NOTE: Spring Session does not work with Grails flash scope without additional work.
|
||||
See https://stackoverflow.com/a/43311427 for an explanation.
|
||||
NOTE: Spring Session will not work with grails flash scope without additional work. +
|
||||
See this answer for an explanation: https://stackoverflow.com/a/43311427
|
||||
|
||||
@@ -3,95 +3,100 @@ Rob Winch
|
||||
:toc:
|
||||
|
||||
This guide describes how to configure Spring Session to use custom cookies with Java Configuration.
|
||||
The guide assumes you have already link:./httpsession.html[set up Spring Session in your project].
|
||||
The guide assumes you have already link:./httpsession.html[setup Spring Session in your project].
|
||||
|
||||
NOTE: You can find the completed guide in the <<custom-cookie-sample, Custom Cookie sample application>>.
|
||||
NOTE: The completed guide can be found in the <<custom-cookie-sample, Custom Cookie sample application>>.
|
||||
|
||||
[[custom-cookie-spring-configuration]]
|
||||
== Spring Java Configuration
|
||||
|
||||
Once you have set up Spring Session, you can customize how the session cookie is written by exposing a `CookieSerializer` as a Spring bean.
|
||||
Spring Session comes with `DefaultCookieSerializer`.
|
||||
Exposing the `DefaultCookieSerializer` as a Spring bean augments the existing configuration when you use configurations like `@EnableRedisHttpSession`.
|
||||
The following example shows how to customize Spring Session's cookie:
|
||||
Once you have setup Spring Session you can easily customize how the session cookie is written by exposing a `CookieSerializer` as a Spring Bean.
|
||||
Out of the box, Spring Session comes with `DefaultCookieSerializer`.
|
||||
Simply exposing the `DefaultCookieSerializer` as a Spring Bean will augment the existing configuration when using configurations like `@EnableRedisHttpSession`.
|
||||
You can find an example of customizing Spring Session's cookie below:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}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]+)$`.
|
||||
<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.
|
||||
====
|
||||
If the regular expression does not match, no domain is set and the existing domain will be used.
|
||||
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] will be used as the domain.
|
||||
This means that a request to https://child.example.com will set the domain to example.com.
|
||||
However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ will leave the cookie unset and thus still work in development without any changes 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].
|
||||
[WARNING]
|
||||
====
|
||||
It is important to note that users should only match on valid domain characters since the domain name is reflected in the response.
|
||||
This is prevent a malicious user from performing attacks like https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
|
||||
====
|
||||
|
||||
[[custom-cookie-options]]
|
||||
== Configuration Options
|
||||
|
||||
The following configuration options are available:
|
||||
The configuration options available are:
|
||||
|
||||
* `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.
|
||||
* `cookieName` - the name of the cookie to use
|
||||
Default "SESSION"
|
||||
* `useSecureCookie` - specify if a secure cookie be used
|
||||
Default use value of `HttpServletRequest.isSecure()` at the time of creation.
|
||||
* `cookiePath` - the path of the cookie
|
||||
Default is context root
|
||||
* `cookieMaxAge` - specifies the max age of the cookie to be set at the time the session is created.
|
||||
Default is -1 which indicates the cookie will 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.
|
||||
With some implementations (i.e. Redis) this provides no performance benefit.
|
||||
However, this 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 will likely require 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 used to extract the value of the cookie domain.
|
||||
If the regular expression does not match, no domain is set and the existing domain will be used.
|
||||
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] will be used as the domain.
|
||||
|
||||
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].
|
||||
[WARNING]
|
||||
====
|
||||
It is important to note that users should only match on valid domain characters since the domain name is reflected in the response.
|
||||
This is prevent a malicious user from performing attacks like https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
|
||||
====
|
||||
|
||||
|
||||
[[custom-cookie-sample]]
|
||||
== `custom-cookie` Sample Application
|
||||
== custom-cookie Sample Application
|
||||
|
||||
This section describes how to work with the `custom-cookie` sample application.
|
||||
|
||||
=== Running the `custom-cookie` Sample Application
|
||||
|
||||
=== Running the custom-cookie Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-custom-cookie:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `custom-cookie` Sample Application
|
||||
=== Exploring the custom-cookie Sample Application
|
||||
|
||||
Now you can use the application. Fill out the form with the following information:
|
||||
Try using the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
* **Attribute Name:** _username_
|
||||
* **Attribute Value:** _rob_
|
||||
|
||||
Now click the *Set Attribute* button.
|
||||
Now click the **Set Attribute** button.
|
||||
You should now see the values displayed in the table.
|
||||
|
||||
If you look at the cookies for the application, you can see the cookie is saved to the custom name of `JSESSIONID`.
|
||||
If you look at the cookies for the application, you can see the cookie is saved to the custom name of JSESSIONID
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
Tommy Ludwig; Rob Winch
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session along with Spring Security when you use Hazelcast as your data store.
|
||||
It assumes that you have already applied Spring Security to your application.
|
||||
This guide describes how to use Spring Session along with Spring Security using Hazelcast as your data store.
|
||||
It assumes you have already applied Spring Security to your application.
|
||||
|
||||
NOTE: You cand find the completed guide in the <<hazelcast-spring-security-sample, Hazelcast Spring Security sample application>>.
|
||||
NOTE: The completed guide can be found in the <<hazelcast-spring-security-sample, Hazelcast Spring Security sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -32,13 +30,11 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -52,14 +48,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -68,7 +62,6 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
@@ -77,146 +70,126 @@ endif::[]
|
||||
== Spring Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{docs-test-dir}docs/http/HazelcastHttpSessionConfig.java[tags=config]
|
||||
----
|
||||
|
||||
<1> The `@EnableHazelcastHttpSession` annotation creates a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by Hazelcast.
|
||||
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
|
||||
<1> The `@EnableHazelcastHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by Hazelcast.
|
||||
<2> In order to support retrieval of sessions by principal name index, appropriate `ValueExtractor` needs to be registered.
|
||||
Spring Session provides `PrincipalNameExtractor` for this purpose.
|
||||
<3> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
|
||||
By default, the application starts and connects to an embedded instance of Hazelcast.
|
||||
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
||||
====
|
||||
By default, an embedded instance of Hazelcast is started and connected to by the application.
|
||||
For more information on configuring Hazelcast, refer to the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
||||
|
||||
== Servlet Container Initialization
|
||||
|
||||
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<security-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `SessionConfig` class.
|
||||
Since our application is already loading Spring configuration by using our `SecurityInitializer` class, we can add our `SessionConfig` class to it.
|
||||
The following listing shows how to do so:
|
||||
Since our application is already loading Spring configuration using our `SecurityInitializer` class, we can simply add our `SessionConfig` class to it.
|
||||
|
||||
====
|
||||
.src/main/java/sample/SecurityInitializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/hazelcast/src/main/java/sample/SecurityInitializer.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
It is extremely important that Spring Session's `springSessionRepositoryFilter` is invoked before Spring Security's `springSecurityFilterChain`.
|
||||
Doing so ensures that the `HttpSession` that Spring Security uses is backed by Spring Session.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this doing so easy.
|
||||
The following example shows how to do so:
|
||||
This ensures that the `HttpSession` that Spring Security uses is backed by Spring Session.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this extremely easy.
|
||||
You can find an example below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/Initializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/hazelcast/src/main/java/sample/Initializer.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The name of our class (`Initializer`) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
|
||||
By extending `AbstractHttpSessionApplicationInitializer`, we ensure that the Spring Bean named `springSessionRepositoryFilter` is registered with our servlet container for every request before Spring Security's `springSecurityFilterChain`.
|
||||
By extending `AbstractHttpSessionApplicationInitializer` we ensure that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request before Spring Security's `springSecurityFilterChain`.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[hazelcast-spring-security-sample]]
|
||||
== Hazelcast Spring Security Sample Application
|
||||
|
||||
This section describes how to work with the Hazelcast Spring Security sample application.
|
||||
|
||||
=== Running the Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Hazelcast will run in embedded mode with your application by default, but if you want to connect
|
||||
to a stand alone instance instead, you can configure it by following the instructions in the
|
||||
https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-hazelcast:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: By default, Hazelcast runs in embedded mode with your application.
|
||||
However, if you want to connect to a standalone instance instead, you can configure it by following the instructions in the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the Security Sample Application
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
You can now try using the application.
|
||||
To do so, enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
You should now see a message indicating that your are logged in with the user entered previously.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
The user's information is stored in Hazelcast rather than Tomcat's `HttpSession` implementation.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Hazelcast.
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Hazelcast.
|
||||
Spring Session replaces the `HttpSession` with an implementation that is backed by a `Map` in Hazelcast.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`, it is then persisted into Hazelcast.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Hazelcast.
|
||||
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named `SESSION` in your browser. That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
=== Interacting with the Data Store
|
||||
=== Interact with the data store
|
||||
|
||||
You can remove the session by using https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-java-client[a Java client],
|
||||
If you like, you can remove the session using https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-java-client[a Java client],
|
||||
https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#other-client-implementations[one of the other clients], or the
|
||||
https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#management-center[management center].
|
||||
|
||||
==== Using the Console
|
||||
==== Using the console
|
||||
|
||||
For example, to remove the session by using the management center console after connecting to your Hazelcast node, run the following commands:
|
||||
For example, using the management center console after connecting to your Hazelcast node:
|
||||
|
||||
====
|
||||
----
|
||||
default> ns spring:session:sessions
|
||||
spring:session:sessions> m.clear
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Hazelcast documentation has instructions for https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#executing-console-commands[the console].
|
||||
|
||||
Alternatively, you can also delete the explicit key. Enter the following into the console, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your `SESSION` cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into the console ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
spring:session:sessions> m.remove 7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
|
||||
==== Using the REST API
|
||||
|
||||
As described in the section of the documentation that cover other clients, there is a
|
||||
As described in the other clients section of the documentation, there is a
|
||||
https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#rest-client[REST API]
|
||||
provided by the Hazelcast node(s).
|
||||
|
||||
For example, you could delete an individual key as follows (being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie):
|
||||
For example, you could delete an individual key as follows (replacing `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie):
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v -X DELETE http://localhost:xxxxx/hazelcast/rest/maps/spring:session:sessions/7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The port number of the Hazelcast node is printed to the console on startup. Replace `xxxxx` with the port number.
|
||||
TIP: The port number of the Hazelcast node will be printed to the console on startup. Replace `xxxxx` above with the port number.
|
||||
|
||||
Now you can see that you are no longer authenticated with this session.
|
||||
Now observe that you are no longer authenticated with this session.
|
||||
|
||||
@@ -4,14 +4,12 @@ Rob Winch, Vedran Pavić
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` with Java Configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-sample, httpsession-jdbc sample application>>.
|
||||
NOTE: The completed guide can be found in the <<httpsession-jdbc-sample, httpsession-jdbc sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -32,13 +30,11 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -52,14 +48,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -68,7 +62,6 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
@@ -78,95 +71,82 @@ endif::[]
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/jdbc/src/main/java/sample/Config.java[tags=class]
|
||||
----
|
||||
|
||||
<1> The `@EnableJdbcHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter`.
|
||||
That bean implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by a relational database.
|
||||
<2> We create a `dataSource` that connects Spring Session to an embedded instance of an H2 database.
|
||||
We configure the H2 database to create database tables by using the SQL script that is included in Spring Session.
|
||||
<1> The `@EnableJdbcHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by a relational database.
|
||||
<2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database.
|
||||
We configure the H2 database to create database tables using the SQL script which is included in Spring Session.
|
||||
<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`.
|
||||
====
|
||||
|
||||
For additional information on how to configure data access related concerns, see the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
|
||||
For additional information on how to configure data access related concerns, please refer to the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
|
||||
|
||||
== Java Servlet Container Initialization
|
||||
|
||||
Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` to make both of these steps easy.
|
||||
The following example shows how to do so:
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` both of these steps extremely easy.
|
||||
You can find an example below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/Initializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/jdbc/src/main/java/sample/Initializer.java[tags=class]
|
||||
----
|
||||
|
||||
NOTE: The name of our class (Initializer) does not matter.
|
||||
What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
|
||||
<1> The first step is to extend `AbstractHttpSessionApplicationInitializer`.
|
||||
Doing so ensures that the Spring bean named `springSessionRepositoryFilter` is registered with our Servlet Container for every request.
|
||||
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to ensure Spring loads our `Config`.
|
||||
====
|
||||
This ensures that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request.
|
||||
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to easily ensure Spring loads our `Config`.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[httpsession-jdbc-sample]]
|
||||
== `httpsession-jdbc` Sample Application
|
||||
== httpsession-jdbc Sample Application
|
||||
|
||||
This section describes how to work with the `httpsession-jdbc` Sample Application.
|
||||
|
||||
=== Running the `httpsession-jdbc` Sample Application
|
||||
=== Running the httpsession-jdbc Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-jdbc:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `httpsession-jdbc` Sample Application
|
||||
=== Exploring the httpsession-jdbc Sample Application
|
||||
|
||||
Now you can try using the application. To do so, fill out the form with the following information:
|
||||
Try using the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
* **Attribute Name:** _username_
|
||||
* **Attribute Value:** _rob_
|
||||
|
||||
Now click the *Set Attribute* button. You should now see the values displayed in the table.
|
||||
Now click the **Set Attribute** button. You should now see the values displayed in the table.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown in the following listing:
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/SessionServlet.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/jdbc/src/main/java/sample/SessionServlet.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in H2 database.
|
||||
Spring Session creates a cookie named `SESSION` in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in H2 database.
|
||||
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
If you like, you can remove the session by using the H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL).
|
||||
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and see that the attribute we added is no longer displayed.
|
||||
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
= Spring Session - HttpSession (Quick Start)
|
||||
Rob Winch
|
||||
:toc:
|
||||
:version-snapshot: true
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with Java Configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-sample, httpsession sample application>>.
|
||||
NOTE: The completed guide can be found in the <<httpsession-sample, httpsession sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you are using Maven, you must add the following dependencies:
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -37,13 +35,11 @@ If you are using Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -57,14 +53,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -73,7 +67,6 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
@@ -82,22 +75,20 @@ endif::[]
|
||||
== Spring Java Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/redis/src/main/java/sample/Config.java[tags=class]
|
||||
----
|
||||
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by Redis.
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by Redis.
|
||||
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
|
||||
We configure the connection to connect to localhost on the default port (6379).
|
||||
For more information on configuring Spring Data Redis, see the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
====
|
||||
We configure the connection to connect to localhost on the default port (6379)
|
||||
For more information on configuring Spring Data Redis, refer to the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
|
||||
== Java Servlet Container Initialization
|
||||
|
||||
@@ -105,24 +96,22 @@ Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring B
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` to make both of these steps easy.
|
||||
The following shows an example:
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` both of these steps extremely easy.
|
||||
You can find an example below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/Initializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/redis/src/main/java/sample/Initializer.java[tags=class]
|
||||
----
|
||||
|
||||
|
||||
NOTE: The name of our class (`Initializer`) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
|
||||
<1> The first step is to extend `AbstractHttpSessionApplicationInitializer`.
|
||||
Doing so ensures that the Spring Bean by the name of `springSessionRepositoryFilter` is registered with our Servlet Container for every request.
|
||||
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to ensure Spring loads our `Config`.
|
||||
====
|
||||
This ensures that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request.
|
||||
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to easily ensure Spring loads our `Config`.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[httpsession-sample]]
|
||||
@@ -130,66 +119,54 @@ Doing so ensures that the Spring Bean by the name of `springSessionRepositoryFil
|
||||
|
||||
|
||||
|
||||
=== Running the `httpsession` Sample Application
|
||||
=== Running the httpsession Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-redis:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `httpsession` Sample Application
|
||||
=== Exploring the httpsession Sample Application
|
||||
|
||||
Now you can try to use the application. To do so, fill out the form with the following information:
|
||||
Try using the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
* **Attribute Name:** _username_
|
||||
* **Attribute Value:** _rob_
|
||||
|
||||
Now click the *Set Attribute* button. You should now see the values displayed in the table.
|
||||
Now click the **Set Attribute** button. You should now see the values displayed in the table.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown in the following listing:
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/SessionServlet.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/redis/src/main/java/sample/SessionServlet.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Redis.
|
||||
Spring Session creates a cookie named `SESSION` in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
|
||||
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session by using redis-cli.
|
||||
For example, on a Linux based system you can type the following:
|
||||
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and see that the attribute we added is no longer displayed.
|
||||
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
Rob Winch
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when you use REST endpoints.
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using REST endpoints.
|
||||
|
||||
NOTE: You can find the completed guide in the <<rest-sample, rest sample application>>.
|
||||
NOTE: The completed guide can be found in the <<rest-sample, rest sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -37,13 +35,11 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -57,14 +53,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You msut have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -73,7 +67,6 @@ You msut have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
@@ -82,103 +75,83 @@ endif::[]
|
||||
== Spring Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/rest/src/main/java/sample/HttpSessionConfig.java[tags=class]
|
||||
----
|
||||
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by Redis.
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by Redis.
|
||||
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
|
||||
We configure the connection to connect to localhost on the default port (6379).
|
||||
For more information on configuring Spring Data Redis, see the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
We configure the connection to connect to localhost on the default port (6379)
|
||||
For more information on configuring Spring Data Redis, refer to the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
<3> We customize Spring Session's HttpSession integration to use HTTP headers to convey the current session information instead of cookies.
|
||||
====
|
||||
|
||||
== Servlet Container Initialization
|
||||
|
||||
Our <<rest-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
We provide the configuration in our Spring `MvcInitializer`, as the following example shows:
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class. We provide the configuration in our Spring `MvcInitializer` as shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/mvc/MvcInitializer.java
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{samples-dir}javaconfig/rest/src/main/java/sample/mvc/MvcInitializer.java[tags=config]
|
||||
----
|
||||
====
|
||||
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes doing so easy. To do so, extend the class with the default constructor, as the following example shows:
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this extremely easy. Simply extend the class with the default constructor as shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/Initializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/rest/src/main/java/sample/Initializer.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The name of our class (`Initializer`) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[rest-sample]]
|
||||
== `rest` Sample Application
|
||||
== rest Sample Application
|
||||
|
||||
This section describes how to use the `rest` sample application.
|
||||
|
||||
=== Running the `rest` Sample Application
|
||||
=== Running the rest Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-rest:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `rest` Sample Application
|
||||
=== Exploring the rest Sample Application
|
||||
|
||||
You can now try to use the application. To do so, use your favorite REST client to request http://localhost:8080/
|
||||
Try using the application. Use your favorite REST client to request http://localhost:8080/
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/
|
||||
----
|
||||
====
|
||||
|
||||
Note that you are prompted for basic authentication. Provide the following information for the username and password:
|
||||
Observe that we are prompted for basic authentication. Provide the following information for the username and password:
|
||||
|
||||
* *Username* _user-
|
||||
* *Password* _password_
|
||||
* **Username** *user*
|
||||
* **Password** *password*
|
||||
|
||||
Then run the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/ -u user:password
|
||||
----
|
||||
====
|
||||
|
||||
In the output, you should notice the following:
|
||||
In the output you will notice the following:
|
||||
|
||||
====
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
@@ -186,62 +159,44 @@ X-Auth-Token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3
|
||||
|
||||
{"username":"user"}
|
||||
----
|
||||
====
|
||||
|
||||
Specifically, you should notice the following things about our response:
|
||||
Specifically, we notice the following things about our response:
|
||||
|
||||
* The HTTP Status is now a 200.
|
||||
* We have a header a the name of `X-Auth-Token` and that contains a new session ID.
|
||||
* The current username is displayed.
|
||||
* The HTTP Status is now a 200
|
||||
* We have a header with the name of *X-Auth-Token* which contains a new session id
|
||||
* The current username is displayed
|
||||
|
||||
We can now use the `X-Auth-Token` to make another request without providing the username and password again. For example, the following command outputs the username, as before:
|
||||
We can now use the *X-Auth-Token* to make another request without providing the username and password again. For example, the following outputs the username just as before:
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/ -H "X-Auth-Token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3"
|
||||
----
|
||||
====
|
||||
|
||||
The only difference is that the session ID is not provided in the response headers because we are reusing an existing session.
|
||||
The only difference is that the session id is not provided in the response headers because we are reusing an existing session.
|
||||
|
||||
If we invalidate the session, the `X-Auth-Token` is displayed in the response with an empty value. For example, the following command invalidates our session:
|
||||
If we invalidate the session, then the X-Auth-Token is displayed in the response with an empty value. For example, the following will invalidate our session:
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/logout -H "X-Auth-Token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3"
|
||||
----
|
||||
====
|
||||
|
||||
You can see in the output that the `X-Auth-Token` provides an empty `String` indicating that the previous session was invalidated:
|
||||
You will see in the output that the X-Auth-Token provides an empty String indicating that the previous session was invalidated.
|
||||
|
||||
====
|
||||
----
|
||||
HTTP/1.1 204 No Content
|
||||
...
|
||||
X-Auth-Token:
|
||||
----
|
||||
====
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Spring Security interacts with the standard `HttpSession` in `SecurityContextPersistenceFilter`.
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, Spring Security is now persisting the values in Redis.
|
||||
Spring Session creates a header named `X-Auth-Token` in your browser.
|
||||
That header contains the ID of your session.
|
||||
Spring Session creates a header named X-Auth-Token in your browser that contains the id of your session.
|
||||
|
||||
If you like, you can easily see that the session is created in Redis.
|
||||
To do so, create a session by using the following command:
|
||||
If you like, you can easily see that the session is created in Redis. First create a session using the following:
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/ -u user:password
|
||||
----
|
||||
====
|
||||
|
||||
In the output, you should notice the following:
|
||||
In the output you will notice the following:
|
||||
|
||||
===
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
@@ -249,32 +204,17 @@ X-Auth-Token: 7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
|
||||
{"username":"user"}
|
||||
----
|
||||
====
|
||||
|
||||
Now you can remove the session by using redis-cli.
|
||||
For example, on a Linux based system, you can type:
|
||||
Now remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key.
|
||||
To do so, enter the following into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your `SESSION` cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
We can now use the `X-Auth-Token` to make another request with the session we deleted and observe we that are prompted for authentication. For example, the following returns an HTTP 401:
|
||||
We can now use the *X-Auth-Token* to make another request with the session we deleted and observe we are prompted for a authentication. For example, the following returns an HTTP 401:
|
||||
|
||||
====
|
||||
----
|
||||
$ curl -v http://localhost:8080/ -H "X-Auth-Token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3"
|
||||
----
|
||||
====
|
||||
|
||||
@@ -5,13 +5,12 @@ Rob Winch
|
||||
This guide describes how to use Spring Session along with Spring Security.
|
||||
It assumes you have already applied Spring Security to your application.
|
||||
|
||||
NOTE: You can find the completed guide in the <<security-sample, security sample application>>.
|
||||
NOTE: The completed guide can be found in the <<security-sample, security sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -37,13 +36,11 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -57,14 +54,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -73,121 +68,107 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
[[security-spring-configuration]]
|
||||
== Spring Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/security/src/main/java/sample/Config.java[tags=class]
|
||||
----
|
||||
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring bean with the name of `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
<1> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by Redis.
|
||||
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
|
||||
We configure the connection to connect to localhost on the default port (6379)
|
||||
For more information on configuring Spring Data Redis, see the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
====
|
||||
For more information on configuring Spring Data Redis, refer to the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
|
||||
== Servlet Container Initialization
|
||||
|
||||
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<security-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
|
||||
Since our application is already loading Spring configuration by using our `SecurityInitializer` class, we can add our configuration class to it.
|
||||
The following example shows how to do so:
|
||||
Since our application is already loading Spring configuration using our `SecurityInitializer` class, we can simply add our Config class to it.
|
||||
|
||||
====
|
||||
.src/main/java/sample/SecurityInitializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/security/src/main/java/sample/SecurityInitializer.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
It is extremely important that Spring Session's `springSessionRepositoryFilter` is invoked before Spring Security's `springSecurityFilterChain`.
|
||||
This ensures that the `HttpSession` that Spring Security uses is backed by Spring Session.
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes doing so easy.
|
||||
The following example shows how to do so:
|
||||
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this extremely easy.
|
||||
You can find an example below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/Initializer.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}javaconfig/security/src/main/java/sample/Initializer.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
|
||||
|
||||
By extending `AbstractHttpSessionApplicationInitializer`, we ensure that the Spring bean named `springSessionRepositoryFilter` is registered with our Servlet Container for every request before Spring Security's `springSecurityFilterChain` .
|
||||
By extending `AbstractHttpSessionApplicationInitializer` we ensure that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request before Spring Security's `springSecurityFilterChain` .
|
||||
|
||||
[[security-sample]]
|
||||
== `security` Sample Application
|
||||
== security Sample Application
|
||||
|
||||
This section describes how to work with the `security` sample application.
|
||||
|
||||
=== Running the `security` Sample Application
|
||||
|
||||
=== Running the security Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-javaconfig-security:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost.
|
||||
See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `security` Sample Application
|
||||
=== Exploring the security Sample Application
|
||||
|
||||
Now you can use the application. Enter the following to log in:
|
||||
Try using the application. Enter the following to log in:
|
||||
|
||||
* *Username* _user_
|
||||
* *Password* _password_
|
||||
* **Username** _user_
|
||||
* **Password** _password_
|
||||
|
||||
Now click the *Login* button.
|
||||
Now click the **Login** button.
|
||||
You should now see a message indicating your are logged in with the user entered previously.
|
||||
The user's information is stored in Redis rather than Tomcat's `HttpSession` implementation.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Redis.
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
|
||||
Spring Session replaces the `HttpSession` with an implementation that is backed by Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`, it is then persisted into Redis.
|
||||
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Redis.
|
||||
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named `SESSION` in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session using redis-cli. For example, on a Linux-based system you can type the following command:
|
||||
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key.
|
||||
Enter the following command into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your `SESSION` cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and see that we are no longer authenticated.
|
||||
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.
|
||||
|
||||
@@ -4,14 +4,12 @@ Rob Winch, Vedran Pavić
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage a relational to back a web application's `HttpSession` with XML based configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-jdbc-xml-sample, httpsession-jdbc-xml sample application>>.
|
||||
NOTE: The completed guide can be found in the <<httpsession-jdbc-xml-sample, httpsession-jdbc-xml sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you are using Maven, you must add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -32,13 +30,11 @@ If you are using Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -52,14 +48,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -69,7 +63,6 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
----
|
||||
endif::[]
|
||||
====
|
||||
|
||||
// tag::config[]
|
||||
|
||||
@@ -77,10 +70,9 @@ endif::[]
|
||||
== Spring XML Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
The following listing shows how to add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/spring/session.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
@@ -88,94 +80,83 @@ include::{samples-dir}xml/jdbc/src/main/webapp/WEB-INF/spring/session.xml[tags=b
|
||||
----
|
||||
|
||||
<1> We use the combination of `<context:annotation-config/>` and `JdbcHttpSessionConfiguration` because Spring Session does not yet provide XML Namespace support (see https://github.com/spring-projects/spring-session/issues/104[gh-104]).
|
||||
This creates a Spring bean with the name of `springSessionRepositoryFilter`.
|
||||
That bean implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by a relational database.
|
||||
<2> We create a `dataSource` that connects Spring Session to an embedded instance of an H2 database.
|
||||
We configure the H2 database to create database tables by using the SQL script that is included in Spring Session.
|
||||
This creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by a relational database.
|
||||
<2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database.
|
||||
We configure the H2 database to create database tables using the SQL script which is included in Spring Session.
|
||||
<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`.
|
||||
====
|
||||
|
||||
For additional information on how to configure data access-related concerns, see the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
|
||||
For additional information on how to configure data access related concerns, please refer to the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
|
||||
|
||||
== XML Servlet Container Initialization
|
||||
|
||||
Our <<httpsession-xml-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
Our <<httpsession-xml-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, we need to instruct Spring to load our `session.xml` configuration.
|
||||
We do so with the following configuration:
|
||||
We do this with the following configuration:
|
||||
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/web.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
include::{samples-dir}xml/jdbc/src/main/webapp/WEB-INF/web.xml[tags=context-param]
|
||||
include::{samples-dir}xml/jdbc/src/main/webapp/WEB-INF/web.xml[tags=listeners]
|
||||
----
|
||||
====
|
||||
|
||||
The https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/core.html#context-create[`ContextLoaderListener`] reads the `contextConfigLocation` and picks up our session.xml configuration.
|
||||
The https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/core.html#context-create[ContextLoaderListener] reads the contextConfigLocation and picks up our session.xml configuration.
|
||||
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
The following snippet performs this last step for us:
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/web.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
include::{samples-dir}xml/jdbc/src/main/webapp/WEB-INF/web.xml[tags=springSessionRepositoryFilter]
|
||||
----
|
||||
====
|
||||
|
||||
The https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework/web/filter/DelegatingFilterProxy.html[`DelegatingFilterProxy`] looks up a bean named `springSessionRepositoryFilter` and casts it to a `Filter`.
|
||||
For every request on which `DelegatingFilterProxy` is invoked, the `springSessionRepositoryFilter` is invoked.
|
||||
The https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework/web/filter/DelegatingFilterProxy.html[DelegatingFilterProxy] will look up a Bean by the name of `springSessionRepositoryFilter` and cast it to a `Filter`.
|
||||
For every request that `DelegatingFilterProxy` is invoked, the `springSessionRepositoryFilter` will be invoked.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[httpsession-jdbc-xml-sample]]
|
||||
== `httpsession-jdbc-xml` Sample Application
|
||||
== httpsession-jdbc-xml Sample Application
|
||||
|
||||
This section describes how to work with the `httpsession-jdbc-xml` Sample Application.
|
||||
|
||||
=== Running the `httpsession-jdbc-xml` Sample Application
|
||||
=== Running the httpsession-jdbc-xml Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
====
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-xml-jdbc:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `httpsession-jdbc-xml` Sample Application
|
||||
=== Exploring the httpsession-jdbc-xml Sample Application
|
||||
|
||||
Now you can try using the application. To do so, fill out the form with the following information:
|
||||
Try using the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
* **Attribute Name:** _username_
|
||||
* **Attribute Value:** _rob_
|
||||
|
||||
Now click the *Set Attribute* button. You should now see the values displayed in the table.
|
||||
Now click the **Set Attribute** button. You should now see the values displayed in the table.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
We interact with the standard `HttpSession` in the following `SessionServlet`:
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/SessionServlet.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}xml/jdbc/src/main/java/sample/SessionServlet.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in the H2 database.
|
||||
Spring Session creates a cookie named `SESSION` in your browser. That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in H2 database.
|
||||
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session by using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
|
||||
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.
|
||||
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
Rob Winch
|
||||
:toc:
|
||||
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with XML-based configuration.
|
||||
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` with XML based configuration.
|
||||
|
||||
NOTE: You can find the completed guide in the <<httpsession-xml-sample, httpsession-xml sample application>>.
|
||||
NOTE: The completed guide can be found in the <<httpsession-xml-sample, httpsession-xml sample application>>.
|
||||
|
||||
== Updating Dependencies
|
||||
Before you use Spring Session, you must update your dependencies.
|
||||
If you use Maven, you must add the following dependencies:
|
||||
Before you use Spring Session, you must ensure to update your dependencies.
|
||||
If you are using Maven, ensure to add the following dependencies:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
[subs="verbatim,attributes"]
|
||||
@@ -36,13 +35,11 @@ If you use Maven, you must add the following dependencies:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
ifeval::["{version-snapshot}" == "true"]
|
||||
Since we are using a SNAPSHOT version, we need to add the Spring Snapshot Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -56,14 +53,12 @@ You must have the following in your pom.xml:
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
ifeval::["{version-milestone}" == "true"]
|
||||
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
|
||||
You must have the following in your pom.xml:
|
||||
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
|
||||
Ensure you have the following in your pom.xml:
|
||||
|
||||
====
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
@@ -72,7 +67,6 @@ You must have the following in your pom.xml:
|
||||
<url>https://repo.spring.io/libs-milestone</url>
|
||||
</repository>
|
||||
----
|
||||
====
|
||||
endif::[]
|
||||
|
||||
// tag::config[]
|
||||
@@ -81,10 +75,9 @@ endif::[]
|
||||
== Spring XML Configuration
|
||||
|
||||
After adding the required dependencies, we can create our Spring configuration.
|
||||
The Spring configuration is responsible for creating a servlet filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
To do so, add the following Spring Configuration:
|
||||
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
|
||||
Add the following Spring Configuration:
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/spring/session.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
@@ -92,13 +85,12 @@ include::{samples-dir}xml/redis/src/main/webapp/WEB-INF/spring/session.xml[tags=
|
||||
----
|
||||
|
||||
<1> We use the combination of `<context:annotation-config/>` and `RedisHttpSessionConfiguration` because Spring Session does not yet provide XML Namespace support (see https://github.com/spring-projects/spring-session/issues/104[gh-104]).
|
||||
This creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements `Filter`.
|
||||
The filter is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance, Spring Session is backed by Redis.
|
||||
This creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
|
||||
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
|
||||
In this instance Spring Session is backed by Redis.
|
||||
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
|
||||
We configure the connection to connect to localhost on the default port (6379)
|
||||
For more information on configuring Spring Data Redis, see the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
====
|
||||
For more information on configuring Spring Data Redis, refer to the https://docs.spring.io/spring-data/data-redis/docs/{spring-data-redis-version}/reference/html/[reference documentation].
|
||||
|
||||
== XML Servlet Container Initialization
|
||||
|
||||
@@ -106,99 +98,83 @@ Our <<httpsession-xml-spring-configuration,Spring Configuration>> created a Spri
|
||||
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
|
||||
|
||||
In order for our `Filter` to do its magic, we need to instruct Spring to load our `session.xml` configuration.
|
||||
We can do so with the following configuration:
|
||||
We do this with the following configuration:
|
||||
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/web.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
include::{samples-dir}xml/redis/src/main/webapp/WEB-INF/web.xml[tags=context-param]
|
||||
include::{samples-dir}xml/redis/src/main/webapp/WEB-INF/web.xml[tags=listeners]
|
||||
----
|
||||
====
|
||||
|
||||
The https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/core.html#context-create[`ContextLoaderListener`] reads the contextConfigLocation and picks up our session.xml configuration.
|
||||
The https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/core.html#context-create[ContextLoaderListener] reads the contextConfigLocation and picks up our session.xml configuration.
|
||||
|
||||
Last, we need to ensure that our Servlet Container (that is, Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
|
||||
The following snippet performs this last step for us:
|
||||
|
||||
====
|
||||
.src/main/webapp/WEB-INF/web.xml
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
include::{samples-dir}xml/redis/src/main/webapp/WEB-INF/web.xml[tags=springSessionRepositoryFilter]
|
||||
----
|
||||
====
|
||||
|
||||
The https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework/web/filter/DelegatingFilterProxy.html[`DelegatingFilterProxy`] looks up a Bean by the name of `springSessionRepositoryFilter` and cast it to a `Filter`.
|
||||
For every request that `DelegatingFilterProxy` is invoked, the `springSessionRepositoryFilter` is invoked.
|
||||
The https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework/web/filter/DelegatingFilterProxy.html[DelegatingFilterProxy] will look up a Bean by the name of `springSessionRepositoryFilter` and cast it to a `Filter`.
|
||||
For every request that `DelegatingFilterProxy` is invoked, the `springSessionRepositoryFilter` will be invoked.
|
||||
|
||||
// end::config[]
|
||||
|
||||
[[httpsession-xml-sample]]
|
||||
== `httpsession-xml` Sample Application
|
||||
== httpsession-xml Sample Application
|
||||
|
||||
This section describes how to work with the `httpsession-xml` sample application.
|
||||
|
||||
=== Running the `httpsession-xml` Sample Application
|
||||
=== Running the httpsession-xml Sample Application
|
||||
|
||||
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
|
||||
|
||||
NOTE: For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
[NOTE]
|
||||
====
|
||||
For the sample to work, you must https://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
|
||||
====
|
||||
|
||||
----
|
||||
$ ./gradlew :spring-session-sample-xml-redis:tomcatRun
|
||||
----
|
||||
====
|
||||
|
||||
You should now be able to access the application at http://localhost:8080/
|
||||
|
||||
=== Exploring the `httpsession-xml` Sample Application
|
||||
=== Exploring the httpsession-xml Sample Application
|
||||
|
||||
Now you can try using the application. Fill out the form with the following information:
|
||||
Try using the application. Fill out the form with the following information:
|
||||
|
||||
* *Attribute Name:* _username_
|
||||
* *Attribute Value:* _rob_
|
||||
* **Attribute Name:** _username_
|
||||
* **Attribute Value:** _rob_
|
||||
|
||||
Now click the *Set Attribute* button. You should now see the values displayed in the table.
|
||||
Now click the **Set Attribute** button. You should now see the values displayed in the table.
|
||||
|
||||
=== How Does It Work?
|
||||
=== How does it work?
|
||||
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown in the following listing:
|
||||
We interact with the standard `HttpSession` in the `SessionServlet` shown below:
|
||||
|
||||
====
|
||||
.src/main/java/sample/SessionServlet.java
|
||||
[source,java]
|
||||
----
|
||||
include::{samples-dir}xml/redis/src/main/java/sample/SessionServlet.java[tags=class]
|
||||
----
|
||||
====
|
||||
|
||||
Instead of using Tomcat's `HttpSession`, we persist the values in Redis.
|
||||
Spring Session creates a cookie named SESSION in your browser.
|
||||
That cookie contains the ID of your session.
|
||||
You can view the cookies (with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
|
||||
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
|
||||
Go ahead and view the cookies (click for help with https://developers.google.com/web/tools/chrome-devtools/manage-data/cookies[Chrome] or https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector[Firefox]).
|
||||
|
||||
You can remove the session using redis-cli.
|
||||
For example, on a Linux based system you can type the following:
|
||||
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli keys '*' | xargs redis-cli del
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The Redis documentation has instructions for https://redis.io/topics/quickstart[installing redis-cli].
|
||||
|
||||
Alternatively, you can also delete the explicit key. To do so, enter the following into your terminal, being sure to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
|
||||
|
||||
====
|
||||
----
|
||||
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
|
||||
----
|
||||
====
|
||||
|
||||
Now you can visit the application at http://localhost:8080/ and see that the attribute we added is no longer displayed.
|
||||
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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,9 @@ public class FindByIndexNameSessionRepositoryTests {
|
||||
// tag::findby-username[]
|
||||
String username = "username";
|
||||
Map<String, Session> sessionIdToSession = this.sessionRepository
|
||||
.findByPrincipalName(username);
|
||||
.findByIndexNameAndIndexValue(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
username);
|
||||
// end::findby-username[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -43,7 +43,6 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
|
||||
|
||||
/**
|
||||
* @author rwinch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = RememberMeSecurityConfiguration.class)
|
||||
@@ -87,6 +86,5 @@ public class RememberMeSecurityConfigurationTests<T extends Session> {
|
||||
.isEqualTo(Duration.ofDays(30));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// end::class[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -43,7 +43,6 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
|
||||
|
||||
/**
|
||||
* @author rwinch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration
|
||||
@@ -87,6 +86,5 @@ public class RememberMeSecurityConfigurationXmlTests<T extends Session> {
|
||||
.isEqualTo(Duration.ofDays(30));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// end::class[]
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
version=2.1.6.RELEASE
|
||||
springBootVersion=2.0.8.RELEASE
|
||||
version=2.0.11.BUILD-SNAPSHOT
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Californium-SR8'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.1.7.RELEASE'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Lovelace-SR8'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.1.5.RELEASE'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.11.2'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Bismuth-SR1'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.0.13.RELEASE'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-SR14'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.0.12.RELEASE'
|
||||
mavenBom 'org.testcontainers:testcontainers-bom:1.10.5'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.11.4') {
|
||||
dependencySet(group: 'com.hazelcast', version: '3.9.4') {
|
||||
entry 'hazelcast'
|
||||
entry 'hazelcast-client'
|
||||
}
|
||||
|
||||
dependency 'com.h2database:h2:1.4.199'
|
||||
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.2.2.jre8'
|
||||
dependency 'com.zaxxer:HikariCP:3.3.1'
|
||||
dependency 'com.h2database:h2:1.4.197'
|
||||
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.0.0.jre8'
|
||||
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
|
||||
dependency 'io.lettuce:lettuce-core:5.1.6.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:4.0.1'
|
||||
dependency 'javax.servlet:javax.servlet-api:3.1.0'
|
||||
dependency 'junit:junit:4.12'
|
||||
dependency 'mysql:mysql-connector-java:8.0.16'
|
||||
dependency 'mysql:mysql-connector-java:8.0.13'
|
||||
dependency 'org.apache.derby:derby:10.14.2.0'
|
||||
dependency 'org.assertj:assertj-core:3.12.2'
|
||||
dependency 'org.assertj:assertj-core:3.11.1'
|
||||
dependency 'org.hsqldb:hsqldb:2.4.1'
|
||||
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.1'
|
||||
dependency 'org.mockito:mockito-core:2.27.0'
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -44,7 +44,10 @@ public class IndexController {
|
||||
@RequestMapping("/")
|
||||
public String index(Principal principal, Model model) {
|
||||
Collection<? extends Session> usersSessions = this.sessions
|
||||
.findByPrincipalName(principal.getName()).values();
|
||||
.findByIndexNameAndIndexValue(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
principal.getName())
|
||||
.values();
|
||||
model.addAttribute("sessions", usersSessions);
|
||||
return "index";
|
||||
}
|
||||
@@ -53,8 +56,9 @@ public class IndexController {
|
||||
@RequestMapping(value = "/sessions/{sessionIdToDelete}", method = RequestMethod.DELETE)
|
||||
public String removeSession(Principal principal,
|
||||
@PathVariable String sessionIdToDelete) {
|
||||
Set<String> usersSessionIds = this.sessions
|
||||
.findByPrincipalName(principal.getName()).keySet();
|
||||
Set<String> usersSessionIds = this.sessions.findByIndexNameAndIndexValue(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
principal.getName()).keySet();
|
||||
if (usersSessionIds.contains(sessionIdToDelete)) {
|
||||
this.sessions.deleteById(sessionIdToDelete);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -34,7 +34,6 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
|
||||
|
||||
/**
|
||||
* @author Eddú Meléndez
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@AutoConfigureMockMvc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -36,7 +36,7 @@ public class LoginPage extends BasePage {
|
||||
}
|
||||
|
||||
public void assertAt() {
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Login Page");
|
||||
}
|
||||
|
||||
public Form form() {
|
||||
@@ -51,7 +51,7 @@ public class LoginPage extends BasePage {
|
||||
@FindBy(name = "password")
|
||||
private WebElement password;
|
||||
|
||||
@FindBy(tagName = "button")
|
||||
@FindBy(name = "submit")
|
||||
private WebElement button;
|
||||
|
||||
public Form(SearchContext context) {
|
||||
|
||||
@@ -50,7 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@AutoConfigureMockMvc
|
||||
public class HttpRedisJsonTest {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@SpringBootTest
|
||||
public class RedisSerializerTest {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@SpringSessionRedisOperations
|
||||
private RedisTemplate<Object, Object> sessionRedisTemplate;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -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:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -36,7 +36,7 @@ public class LoginPage extends BasePage {
|
||||
}
|
||||
|
||||
public void assertAt() {
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Login Page");
|
||||
}
|
||||
|
||||
public Form form() {
|
||||
@@ -51,7 +51,7 @@ public class LoginPage extends BasePage {
|
||||
@FindBy(name = "password")
|
||||
private WebElement password;
|
||||
|
||||
@FindBy(tagName = "button")
|
||||
@FindBy(name = "submit")
|
||||
private WebElement button;
|
||||
|
||||
public Form(SearchContext context) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -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:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -23,6 +23,5 @@ dependencies {
|
||||
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
testCompile "org.springframework.security:spring-security-test"
|
||||
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
testCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -42,7 +42,7 @@ import org.springframework.web.socket.sockjs.client.SockJsClient;
|
||||
import org.springframework.web.socket.sockjs.client.Transport;
|
||||
import org.springframework.web.socket.sockjs.client.WebSocketTransport;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
public class ApplicationTests {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Value("${local.server.port}")
|
||||
private String port;
|
||||
@@ -70,8 +70,8 @@ public class ApplicationTests {
|
||||
ListenableFuture<WebSocketSession> wsSession = sockJsClient.doHandshake(
|
||||
this.webSocketHandler, "ws://localhost:" + this.port + "/sockjs");
|
||||
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(() -> wsSession.get().sendMessage(new TextMessage("a")));
|
||||
assertThatThrownBy(() -> wsSession.get().sendMessage(new TextMessage("a")))
|
||||
.isInstanceOf(ExecutionException.class);
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
@@ -5,7 +5,7 @@ dependencyManagement {
|
||||
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1'
|
||||
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02'
|
||||
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.32.0'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.29.3'
|
||||
dependency 'org.slf4j:jcl-over-slf4j:1.7.25'
|
||||
dependency 'org.slf4j:log4j-over-slf4j:1.7.25'
|
||||
dependency 'org.webjars:bootstrap:2.3.2'
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
@Profile("embedded-redis")
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -36,7 +36,7 @@ public class LoginPage extends BasePage {
|
||||
}
|
||||
|
||||
public void assertAt() {
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Login Page");
|
||||
}
|
||||
|
||||
public Form form() {
|
||||
@@ -51,7 +51,7 @@ public class LoginPage extends BasePage {
|
||||
@FindBy(name = "password")
|
||||
private WebElement password;
|
||||
|
||||
@FindBy(tagName = "button")
|
||||
@FindBy(name = "submit")
|
||||
private WebElement button;
|
||||
|
||||
public Form(SearchContext context) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
@Profile("embedded-redis")
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -17,6 +17,8 @@ dependencies {
|
||||
testCompile "org.springframework.security:spring-security-test"
|
||||
testCompile "org.assertj:assertj-core"
|
||||
testCompile "org.springframework:spring-test"
|
||||
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
gretty {
|
||||
|
||||
@@ -54,7 +54,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@WebAppConfiguration
|
||||
public class RestMockMvcTests {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Autowired
|
||||
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* @author Pool Dolorier
|
||||
@@ -57,9 +57,9 @@ public class RestTests {
|
||||
public void unauthenticatedUserSentToLogInPage() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
|
||||
assertThatExceptionOfType(HttpClientErrorException.class)
|
||||
.isThrownBy(() -> getForUser(this.baseUrl + "/", headers, String.class))
|
||||
.satisfies((e) -> assertThat(e.getStatusCode())
|
||||
assertThatThrownBy(() -> getForUser(this.baseUrl + "/", headers, String.class))
|
||||
.isInstanceOf(HttpClientErrorException.class)
|
||||
.satisfies((e) -> assertThat(((HttpClientErrorException) e).getStatusCode())
|
||||
.isEqualTo(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
@Profile("embedded-redis")
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -34,7 +34,7 @@ public class LoginPage extends BasePage {
|
||||
@FindBy(name = "password")
|
||||
private WebElement password;
|
||||
|
||||
@FindBy(tagName = "button")
|
||||
@FindBy(css = "input[type='submit']")
|
||||
private WebElement button;
|
||||
|
||||
public LoginPage(WebDriver driver) {
|
||||
@@ -47,7 +47,7 @@ public class LoginPage extends BasePage {
|
||||
}
|
||||
|
||||
public void assertAt() {
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Please sign in");
|
||||
assertThat(getDriver().getTitle()).isEqualTo("Login Page");
|
||||
}
|
||||
|
||||
public HomePage login(String user, String password) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
@Profile("embedded-redis")
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
@Profile("embedded-redis")
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
@Bean
|
||||
public GenericContainer redisContainer() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -2,15 +2,12 @@ rootProject.name = 'spring-session-build'
|
||||
|
||||
FileTree buildFiles = fileTree(rootDir) {
|
||||
include '**/*.gradle'
|
||||
exclude 'build', '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out'
|
||||
exclude '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*'
|
||||
exclude '**/grails3'
|
||||
gradle.startParameter.projectProperties.get('excludeProjects')?.split(',')?.each { excludeProject ->
|
||||
exclude excludeProject
|
||||
}
|
||||
}
|
||||
|
||||
String rootDirPath = rootDir.absolutePath + File.separator
|
||||
buildFiles.each { buildFile ->
|
||||
buildFiles.each { File buildFile ->
|
||||
if (buildFile.name == 'build.gradle') {
|
||||
String buildFilePath = buildFile.parentFile.absolutePath
|
||||
String projectPath = buildFilePath.replace(rootDirPath, '').replace(File.separator, ':')
|
||||
|
||||
@@ -6,7 +6,6 @@ dependencies {
|
||||
compile "org.springframework:spring-jcl"
|
||||
|
||||
optional "io.projectreactor:reactor-core"
|
||||
optional "javax.annotation:javax.annotation-api"
|
||||
optional "javax.servlet:javax.servlet-api"
|
||||
optional "org.springframework:spring-context"
|
||||
optional "org.springframework:spring-jdbc"
|
||||
|
||||
@@ -19,22 +19,27 @@ package org.springframework.session;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extends a basic {@link SessionRepository} to allow finding sessions by the specified
|
||||
* index name and index value.
|
||||
* Extends a basic {@link SessionRepository} to allow finding a session id by the
|
||||
* principal name. The principal name is defined by the {@link Session} attribute with the
|
||||
* name {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}.
|
||||
*
|
||||
* @param <S> the type of Session being managed by this
|
||||
* {@link FindByIndexNameSessionRepository}
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public interface FindByIndexNameSessionRepository<S extends Session>
|
||||
extends SessionRepository<S> {
|
||||
|
||||
/**
|
||||
* A session index that contains the current principal name (i.e. username).
|
||||
* <p>
|
||||
* It is the responsibility of the developer to ensure the index is populated since
|
||||
* Spring Session is not aware of the authentication mechanism being used.
|
||||
* A common session attribute that contains the current principal name (i.e.
|
||||
* username).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It is the responsibility of the developer to ensure the attribute is populated
|
||||
* since Spring Session is not aware of the authentication mechanism being used.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.1
|
||||
*/
|
||||
@@ -42,34 +47,17 @@ public interface FindByIndexNameSessionRepository<S extends Session>
|
||||
.concat(".PRINCIPAL_NAME_INDEX_NAME");
|
||||
|
||||
/**
|
||||
* Find a {@link Map} of the session id to the {@link Session} of all sessions that
|
||||
* contain the specified index name index value.
|
||||
* Find a Map of the session id to the {@link Session} of all sessions that contain
|
||||
* the session attribute with the name
|
||||
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the value of
|
||||
* the specified principal name.
|
||||
*
|
||||
* @param indexName the name of the index (i.e.
|
||||
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
|
||||
* @param indexValue the value of the index to search for.
|
||||
* @return a {@code Map} (never {@code null}) of the session id to the {@code Session}
|
||||
* of all sessions that contain the specified index name and index value. If no
|
||||
* results are found, an empty {@code Map} is returned.
|
||||
* @return a Map (never null) of the session id to the {@link Session} of all sessions
|
||||
* that contain the session specified index name and the value of the specified index
|
||||
* name. If no results are found, an empty Map is returned.
|
||||
*/
|
||||
Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
|
||||
|
||||
/**
|
||||
* Find a {@link Map} of the session id to the {@link Session} of all sessions that
|
||||
* contain the index with the name
|
||||
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the
|
||||
* specified principal name.
|
||||
*
|
||||
* @param principalName the principal name
|
||||
* @return a {@code Map} (never {@code null}) of the session id to the {@code Session}
|
||||
* of all sessions that contain the specified principal name. If no results are found,
|
||||
* an empty {@code Map} is returned.
|
||||
* @since 2.1.0
|
||||
*/
|
||||
default Map<String, S> findByPrincipalName(String principalName) {
|
||||
|
||||
return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public final class MapSession implements Session, Serializable {
|
||||
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
|
||||
|
||||
private String id;
|
||||
private final String originalId;
|
||||
private String originalId;
|
||||
private Map<String, Object> sessionAttrs = new HashMap<>();
|
||||
private Instant creationTime = Instant.now();
|
||||
private Instant lastAccessedTime = this.creationTime;
|
||||
@@ -132,6 +132,10 @@ public final class MapSession implements Session, Serializable {
|
||||
return this.originalId;
|
||||
}
|
||||
|
||||
void setOriginalId(String originalId) {
|
||||
this.originalId = originalId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
String changedId = generateId();
|
||||
|
||||
@@ -73,6 +73,7 @@ public class MapSessionRepository implements SessionRepository<MapSession> {
|
||||
public void save(MapSession session) {
|
||||
if (!session.getId().equals(session.getOriginalId())) {
|
||||
this.sessions.remove(session.getOriginalId());
|
||||
session.setOriginalId(session.getId());
|
||||
}
|
||||
this.sessions.put(session.getId(), new MapSession(session));
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public class ReactiveMapSessionRepository implements ReactiveSessionRepository<M
|
||||
return Mono.fromRunnable(() -> {
|
||||
if (!session.getId().equals(session.getOriginalId())) {
|
||||
this.sessions.remove(session.getOriginalId());
|
||||
session.setOriginalId(session.getId());
|
||||
}
|
||||
this.sessions.put(session.getId(), new MapSession(session));
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -65,8 +65,9 @@ public class SpringSessionBackedSessionRegistry<S extends Session>
|
||||
@Override
|
||||
public List<SessionInformation> getAllSessions(Object principal,
|
||||
boolean includeExpiredSessions) {
|
||||
Collection<S> sessions = this.sessionRepository
|
||||
.findByPrincipalName(name(principal)).values();
|
||||
Collection<S> sessions = this.sessionRepository.findByIndexNameAndIndexValue(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
|
||||
name(principal)).values();
|
||||
List<SessionInformation> infos = new ArrayList<>();
|
||||
for (S session : sessions) {
|
||||
if (includeExpiredSessions || !Boolean.TRUE.equals(session
|
||||
|
||||
@@ -16,13 +16,8 @@
|
||||
|
||||
package org.springframework.session.web.http;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -46,22 +41,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DefaultCookieSerializer.class);
|
||||
|
||||
private static final BitSet domainValid = new BitSet(128);
|
||||
|
||||
static {
|
||||
for (char c = '0'; c <= '9'; c++) {
|
||||
domainValid.set(c);
|
||||
}
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
domainValid.set(c);
|
||||
}
|
||||
for (char c = 'A'; c <= 'Z'; c++) {
|
||||
domainValid.set(c);
|
||||
}
|
||||
domainValid.set('.');
|
||||
domainValid.set('-');
|
||||
}
|
||||
|
||||
private String cookieName = "SESSION";
|
||||
|
||||
private Boolean useSecureCookie;
|
||||
@@ -82,8 +61,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
|
||||
private String rememberMeRequestAttribute;
|
||||
|
||||
private String sameSite = "Lax";
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -98,8 +75,7 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (this.cookieName.equals(cookie.getName())) {
|
||||
String sessionId = (this.useBase64Encoding
|
||||
? base64Decode(cookie.getValue())
|
||||
: cookie.getValue());
|
||||
? base64Decode(cookie.getValue()) : cookie.getValue());
|
||||
if (sessionId == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -125,43 +101,38 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
HttpServletRequest request = cookieValue.getRequest();
|
||||
HttpServletResponse response = cookieValue.getResponse();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(this.cookieName).append('=');
|
||||
String value = getValue(cookieValue);
|
||||
if (value != null && value.length() > 0) {
|
||||
validateValue(value);
|
||||
sb.append(value);
|
||||
}
|
||||
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);
|
||||
sb.append("; Expires=")
|
||||
.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
String domain = getDomainName(request);
|
||||
if (domain != null && domain.length() > 0) {
|
||||
validateDomain(domain);
|
||||
sb.append("; Domain=").append(domain);
|
||||
}
|
||||
String path = getCookiePath(request);
|
||||
if (path != null && path.length() > 0) {
|
||||
validatePath(path);
|
||||
sb.append("; Path=").append(path);
|
||||
}
|
||||
if (isSecureCookie(request)) {
|
||||
sb.append("; Secure");
|
||||
}
|
||||
if (this.useHttpOnlyCookie) {
|
||||
sb.append("; HttpOnly");
|
||||
}
|
||||
if (this.sameSite != null) {
|
||||
sb.append("; SameSite=").append(this.sameSite);
|
||||
String requestedCookieValue = cookieValue.getCookieValue();
|
||||
String actualCookieValue = (this.jvmRoute != null)
|
||||
? requestedCookieValue + this.jvmRoute
|
||||
: requestedCookieValue;
|
||||
|
||||
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding
|
||||
? base64Encode(actualCookieValue) : actualCookieValue);
|
||||
sessionCookie.setSecure(isSecureCookie(request));
|
||||
sessionCookie.setPath(getCookiePath(request));
|
||||
String domainName = getDomainName(request);
|
||||
if (domainName != null) {
|
||||
sessionCookie.setDomain(domainName);
|
||||
}
|
||||
|
||||
response.addHeader("Set-Cookie", sb.toString());
|
||||
if (this.useHttpOnlyCookie) {
|
||||
sessionCookie.setHttpOnly(true);
|
||||
}
|
||||
|
||||
if (cookieValue.getCookieMaxAge() < 0) {
|
||||
if (this.rememberMeRequestAttribute != null
|
||||
&& request.getAttribute(this.rememberMeRequestAttribute) != null) {
|
||||
// the cookie is only written at time of session creation, so we rely on
|
||||
// session expiration rather than cookie expiration if remember me is enabled
|
||||
cookieValue.setCookieMaxAge(Integer.MAX_VALUE);
|
||||
}
|
||||
else if (this.cookieMaxAge != null) {
|
||||
cookieValue.setCookieMaxAge(this.cookieMaxAge);
|
||||
}
|
||||
}
|
||||
sessionCookie.setMaxAge(cookieValue.getCookieMaxAge());
|
||||
|
||||
response.addCookie(sessionCookie);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,81 +163,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
return new String(encodedCookieBytes);
|
||||
}
|
||||
|
||||
private String getValue(CookieValue cookieValue) {
|
||||
String requestedCookieValue = cookieValue.getCookieValue();
|
||||
String actualCookieValue = requestedCookieValue;
|
||||
if (this.jvmRoute != null) {
|
||||
actualCookieValue = requestedCookieValue + this.jvmRoute;
|
||||
}
|
||||
if (this.useBase64Encoding) {
|
||||
actualCookieValue = base64Encode(actualCookieValue);
|
||||
}
|
||||
return actualCookieValue;
|
||||
}
|
||||
|
||||
private void validateValue(String value) {
|
||||
int start = 0;
|
||||
int end = value.length();
|
||||
if ((end > 1) && (value.charAt(0) == '"') && (value.charAt(end - 1) == '"')) {
|
||||
start = 1;
|
||||
end--;
|
||||
}
|
||||
char[] chars = value.toCharArray();
|
||||
for (int i = start; i < end; i++) {
|
||||
char c = chars[i];
|
||||
if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c
|
||||
|| c == 0x7f) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid character in cookie value: " + Integer.toString(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getMaxAge(CookieValue cookieValue) {
|
||||
int maxAge = cookieValue.getCookieMaxAge();
|
||||
if (maxAge < 0) {
|
||||
if (this.rememberMeRequestAttribute != null && cookieValue.getRequest()
|
||||
.getAttribute(this.rememberMeRequestAttribute) != null) {
|
||||
// the cookie is only written at time of session creation, so we rely on
|
||||
// session expiration rather than cookie expiration if remember me is
|
||||
// enabled
|
||||
cookieValue.setCookieMaxAge(Integer.MAX_VALUE);
|
||||
}
|
||||
else if (this.cookieMaxAge != null) {
|
||||
cookieValue.setCookieMaxAge(this.cookieMaxAge);
|
||||
}
|
||||
}
|
||||
return cookieValue.getCookieMaxAge();
|
||||
}
|
||||
|
||||
private void validateDomain(String domain) {
|
||||
int i = 0;
|
||||
int cur = -1;
|
||||
int prev;
|
||||
char[] chars = domain.toCharArray();
|
||||
while (i < chars.length) {
|
||||
prev = cur;
|
||||
cur = chars[i];
|
||||
if (!domainValid.get(cur)
|
||||
|| ((prev == '.' || prev == -1) && (cur == '.' || cur == '-'))
|
||||
|| (prev == '-' && cur == '.')) {
|
||||
throw new IllegalArgumentException("Invalid cookie domain: " + domain);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (cur == '.' || cur == '-') {
|
||||
throw new IllegalArgumentException("Invalid cookie domain: " + domain);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePath(String path) {
|
||||
for (char ch : path.toCharArray()) {
|
||||
if (ch < 0x20 || ch > 0x7E || ch == ';') {
|
||||
throw new IllegalArgumentException("Invalid cookie path: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if a Cookie marked as secure should be used. The default is to use the value
|
||||
* of {@link HttpServletRequest#isSecure()}.
|
||||
@@ -422,16 +318,6 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
this.rememberMeRequestAttribute = rememberMeRequestAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the {@code SameSite} cookie directive. The default value is
|
||||
* {@code Lax}.
|
||||
* @param sameSite the SameSite directive value
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public void setSameSite(String sameSite) {
|
||||
this.sameSite = sameSite;
|
||||
}
|
||||
|
||||
private String getDomainName(HttpServletRequest request) {
|
||||
if (this.domainName != null) {
|
||||
return this.domainName;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -24,13 +24,8 @@ import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.servlet.http.HttpSessionBindingEvent;
|
||||
import javax.servlet.http.HttpSessionBindingListener;
|
||||
import javax.servlet.http.HttpSessionContext;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.session.Session;
|
||||
|
||||
/**
|
||||
@@ -38,14 +33,11 @@ import org.springframework.session.Session;
|
||||
*
|
||||
* @param <S> the {@link Session} type
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
* @since 1.1
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class HttpSessionAdapter<S extends Session> implements HttpSession {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(HttpSessionAdapter.class);
|
||||
|
||||
private S session;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
@@ -137,28 +129,7 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
|
||||
@Override
|
||||
public void setAttribute(String name, Object value) {
|
||||
checkState();
|
||||
Object oldValue = this.session.getAttribute(name);
|
||||
this.session.setAttribute(name, value);
|
||||
if (value != oldValue) {
|
||||
if (oldValue instanceof HttpSessionBindingListener) {
|
||||
try {
|
||||
((HttpSessionBindingListener) oldValue).valueUnbound(
|
||||
new HttpSessionBindingEvent(this, name, oldValue));
|
||||
}
|
||||
catch (Throwable th) {
|
||||
logger.error("Error invoking session binding event listener", th);
|
||||
}
|
||||
}
|
||||
if (value instanceof HttpSessionBindingListener) {
|
||||
try {
|
||||
((HttpSessionBindingListener) value)
|
||||
.valueBound(new HttpSessionBindingEvent(this, name, value));
|
||||
}
|
||||
catch (Throwable th) {
|
||||
logger.error("Error invoking session binding event listener", th);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,17 +140,7 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
|
||||
@Override
|
||||
public void removeAttribute(String name) {
|
||||
checkState();
|
||||
Object oldValue = this.session.getAttribute(name);
|
||||
this.session.removeAttribute(name);
|
||||
if (oldValue instanceof HttpSessionBindingListener) {
|
||||
try {
|
||||
((HttpSessionBindingListener) oldValue)
|
||||
.valueUnbound(new HttpSessionBindingEvent(this, name, oldValue));
|
||||
}
|
||||
catch (Throwable th) {
|
||||
logger.error("Error invoking session binding event listener", th);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -87,6 +87,12 @@ public class SpringSessionWebSessionStore<S extends Session> implements WebSessi
|
||||
return Mono.just(session);
|
||||
}
|
||||
|
||||
public Mono<Void> storeSession(WebSession session) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SpringSessionWebSession springWebSession = (SpringSessionWebSession) session;
|
||||
return this.sessions.save(springWebSession.session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> retrieveSession(String sessionId) {
|
||||
return this.sessions.findById(sessionId)
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class MapSessionTests {
|
||||
|
||||
@@ -38,9 +38,9 @@ public class MapSessionTests {
|
||||
|
||||
@Test
|
||||
public void constructorNullSession() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new MapSession((Session) null))
|
||||
.withMessage("session cannot be null");
|
||||
assertThatThrownBy(() -> new MapSession((Session) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("session cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,9 +70,9 @@ public class MapSessionTests {
|
||||
|
||||
@Test
|
||||
public void getRequiredAttributeWhenNullThenException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.session.getRequiredAttribute("attrName"))
|
||||
.withMessage("Required attribute 'attrName' is missing.");
|
||||
assertThatThrownBy(() -> this.session.getRequiredAttribute("attrName"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Required attribute 'attrName' is missing.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveMapSessionRepository}.
|
||||
@@ -60,9 +60,9 @@ public class ReactiveMapSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void constructorMapWhenNullThenThrowsIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new ReactiveMapSessionRepository(null))
|
||||
.withMessage("sessions cannot be null");
|
||||
assertThatThrownBy(() -> new ReactiveMapSessionRepository(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("sessions cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
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.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link SpringHttpSessionConfiguration}.
|
||||
@@ -63,9 +63,9 @@ public class SpringHttpSessionConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void noSessionRepositoryConfiguration() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(() -> registerAndRefresh(EmptyConfiguration.class))
|
||||
.withMessageContaining("org.springframework.session.SessionRepository");
|
||||
assertThatThrownBy(() -> registerAndRefresh(EmptyConfiguration.class))
|
||||
.isInstanceOf(UnsatisfiedDependencyException.class)
|
||||
.hasMessageContaining("org.springframework.session.SessionRepository");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.session.security;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -75,9 +74,7 @@ public class SpringSessionBackedSessionRegistryTest {
|
||||
.getSessionInformation(SESSION_ID);
|
||||
|
||||
assertThat(sessionInfo.getSessionId()).isEqualTo(SESSION_ID);
|
||||
assertThat(
|
||||
sessionInfo.getLastRequest().toInstant().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(NOW.truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(sessionInfo.getLastRequest().toInstant()).isEqualTo(NOW);
|
||||
assertThat(sessionInfo.getPrincipal()).isEqualTo(USER_NAME);
|
||||
assertThat(sessionInfo.isExpired()).isFalse();
|
||||
}
|
||||
@@ -93,9 +90,7 @@ public class SpringSessionBackedSessionRegistryTest {
|
||||
.getSessionInformation(SESSION_ID);
|
||||
|
||||
assertThat(sessionInfo.getSessionId()).isEqualTo(SESSION_ID);
|
||||
assertThat(
|
||||
sessionInfo.getLastRequest().toInstant().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(NOW.truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(sessionInfo.getLastRequest().toInstant()).isEqualTo(NOW);
|
||||
assertThat(sessionInfo.getPrincipal()).isEqualTo(USER_NAME);
|
||||
assertThat(sessionInfo.isExpired()).isTrue();
|
||||
}
|
||||
@@ -167,8 +162,9 @@ public class SpringSessionBackedSessionRegistryTest {
|
||||
Map<String, Session> sessions = new LinkedHashMap<>();
|
||||
sessions.put(session1.getId(), session1);
|
||||
sessions.put(session2.getId(), session2);
|
||||
when(this.sessionRepository.findByPrincipalName(USER_NAME))
|
||||
.thenReturn(sessions);
|
||||
when(this.sessionRepository.findByIndexNameAndIndexValue(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, USER_NAME))
|
||||
.thenReturn(sessions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.security.web.context.HttpSessionSecurityContextReposi
|
||||
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.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -68,10 +68,9 @@ public class SpringSessionRememberMeServicesTests {
|
||||
@Test
|
||||
public void createWithNullParameter() {
|
||||
this.rememberMeServices = new SpringSessionRememberMeServices();
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(
|
||||
() -> this.rememberMeServices.setRememberMeParameterName(null))
|
||||
.withMessage("rememberMeParameterName cannot be empty or null");
|
||||
assertThatThrownBy(() -> this.rememberMeServices.setRememberMeParameterName(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("rememberMeParameterName cannot be empty or null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
|
||||
@@ -26,14 +26,13 @@ import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import org.springframework.mock.web.MockCookie;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.session.web.http.CookieSerializer.CookieValue;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultCookieSerializer}.
|
||||
@@ -214,9 +213,9 @@ public class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
public void setDomainNameAndDomainNamePatternThrows() {
|
||||
this.serializer.setDomainName("example.com");
|
||||
assertThatExceptionOfType(IllegalStateException.class)
|
||||
.isThrownBy(() -> this.serializer.setDomainNamePattern(".*"))
|
||||
.withMessage("Cannot set both domainName and domainNamePattern");
|
||||
assertThatThrownBy(() -> this.serializer.setDomainNamePattern(".*"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Cannot set both domainName and domainNamePattern");
|
||||
}
|
||||
|
||||
// --- domainNamePattern ---
|
||||
@@ -248,9 +247,9 @@ public class DefaultCookieSerializerTests {
|
||||
@Test
|
||||
public void setDomainNamePatternAndDomainNameThrows() {
|
||||
this.serializer.setDomainNamePattern(".*");
|
||||
assertThatExceptionOfType(IllegalStateException.class)
|
||||
.isThrownBy(() -> this.serializer.setDomainName("example.com"))
|
||||
.withMessage("Cannot set both domainName and domainNamePattern");
|
||||
assertThatThrownBy(() -> this.serializer.setDomainName("example.com"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Cannot set both domainName and domainNamePattern");
|
||||
}
|
||||
|
||||
// --- cookieName ---
|
||||
@@ -274,9 +273,9 @@ public class DefaultCookieSerializerTests {
|
||||
|
||||
@Test
|
||||
public void setCookieNameNullThrows() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.serializer.setCookieName(null))
|
||||
.withMessage("cookieName cannot be null");
|
||||
assertThatThrownBy(() -> this.serializer.setCookieName(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("cookieName cannot be null");
|
||||
}
|
||||
|
||||
// --- cookiePath ---
|
||||
@@ -467,39 +466,6 @@ public class DefaultCookieSerializerTests {
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
}
|
||||
|
||||
// --- sameSite ---
|
||||
|
||||
@Test
|
||||
public void writeCookieDefaultSameSiteLax() {
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeCookieSetSameSiteLax() {
|
||||
this.serializer.setSameSite("Lax");
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Lax");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeCookieSetSameSiteStrict() {
|
||||
this.serializer.setSameSite("Strict");
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isEqualTo("Strict");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeCookieSetSameSiteNull() {
|
||||
this.serializer.setSameSite(null);
|
||||
this.serializer.writeCookieValue(cookieValue(this.sessionId));
|
||||
|
||||
assertThat(getCookie().getSameSite()).isNull();
|
||||
}
|
||||
|
||||
public void setCookieName(String cookieName) {
|
||||
this.cookieName = cookieName;
|
||||
this.serializer.setCookieName(cookieName);
|
||||
@@ -512,8 +478,8 @@ public class DefaultCookieSerializerTests {
|
||||
return new Cookie(name, value);
|
||||
}
|
||||
|
||||
private MockCookie getCookie() {
|
||||
return (MockCookie) this.response.getCookie(this.cookieName);
|
||||
private Cookie getCookie() {
|
||||
return this.response.getCookie(this.cookieName);
|
||||
}
|
||||
|
||||
private String getCookieValue() {
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
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.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link HeaderHttpSessionIdResolver}.
|
||||
@@ -74,9 +74,9 @@ public class HeaderHttpSessionIdResolverTests {
|
||||
|
||||
@Test
|
||||
public void createResolverWithNullHeaderName() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new HeaderHttpSessionIdResolver(null))
|
||||
.withMessage("headerName cannot be null");
|
||||
assertThatThrownBy(() -> new HeaderHttpSessionIdResolver(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("headerName cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -27,8 +27,6 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletContext;
|
||||
@@ -38,8 +36,6 @@ import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.servlet.http.HttpSessionBindingEvent;
|
||||
import javax.servlet.http.HttpSessionBindingListener;
|
||||
import javax.servlet.http.HttpSessionContext;
|
||||
|
||||
import org.assertj.core.data.Offset;
|
||||
@@ -62,7 +58,7 @@ import org.springframework.session.SessionRepository;
|
||||
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.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -1418,132 +1414,16 @@ public class SessionRepositoryFilterTests {
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
public void doesNotImplementOrdered() {
|
||||
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> {
|
||||
assertThatThrownBy(() -> {
|
||||
Ordered o = (Ordered) this.filter;
|
||||
});
|
||||
}).isInstanceOf(ClassCastException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHttpSessionIdResolverNull() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.filter.setHttpSessionIdResolver(null))
|
||||
.withMessage("httpSessionIdResolver cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindListener() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener.getCounter()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindListenerThenUnbind() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
session.removeAttribute(bindingListenerName);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener.getCounter()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindSameListenerTwice() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener.getCounter()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindListenerOverwrite() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener1 = new CountingHttpSessionBindingListener();
|
||||
CountingHttpSessionBindingListener bindingListener2 = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
session.setAttribute(bindingListenerName, bindingListener1);
|
||||
session.setAttribute(bindingListenerName, bindingListener2);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener1.getCounter()).isEqualTo(0);
|
||||
assertThat(bindingListener2.getCounter()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindThrowsException() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
bindingListener.setThrowException();
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener.getCounter()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindingListenerBindListenerThenUnbindThrowsException() throws Exception {
|
||||
String bindingListenerName = "bindingListener";
|
||||
CountingHttpSessionBindingListener bindingListener = new CountingHttpSessionBindingListener();
|
||||
|
||||
doFilter(new DoInFilter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest wrappedRequest) {
|
||||
HttpSession session = wrappedRequest.getSession();
|
||||
session.setAttribute(bindingListenerName, bindingListener);
|
||||
bindingListener.setThrowException();
|
||||
session.removeAttribute(bindingListenerName);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
assertThat(bindingListener.getCounter()).isEqualTo(1);
|
||||
assertThatThrownBy(() -> this.filter.setHttpSessionIdResolver(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("httpSessionIdResolver cannot be null");
|
||||
}
|
||||
|
||||
// --- helper methods
|
||||
@@ -1648,39 +1528,4 @@ public class SessionRepositoryFilterTests {
|
||||
|
||||
}
|
||||
|
||||
private static class CountingHttpSessionBindingListener
|
||||
implements HttpSessionBindingListener {
|
||||
|
||||
private final AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private final AtomicBoolean throwException = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void valueBound(HttpSessionBindingEvent event) {
|
||||
if (this.throwException.get()) {
|
||||
this.throwException.compareAndSet(true, false);
|
||||
throw new RuntimeException("bind exception");
|
||||
}
|
||||
this.counter.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void valueUnbound(HttpSessionBindingEvent event) {
|
||||
if (this.throwException.get()) {
|
||||
this.throwException.compareAndSet(true, false);
|
||||
throw new RuntimeException("unbind exception");
|
||||
}
|
||||
this.counter.decrementAndGet();
|
||||
}
|
||||
|
||||
int getCounter() {
|
||||
return this.counter.get();
|
||||
}
|
||||
|
||||
void setThrowException() {
|
||||
this.throwException.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.session.Session;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -69,9 +69,9 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void constructorWhenNullRepositoryThenThrowsIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new SpringSessionWebSessionStore<S>(null))
|
||||
.withMessage("reactiveSessionRepository cannot be null");
|
||||
assertThatThrownBy(() -> new SpringSessionWebSessionStore<S>(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("reactiveSessionRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -253,6 +253,17 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
.containsExactly(new AbstractMap.SimpleEntry<>(attrName, attrValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storeSessionWhenInvokedThenSessionSaved() {
|
||||
given(this.sessionRepository.save(this.createSession)).willReturn(Mono.empty());
|
||||
WebSession createdSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
this.webSessionStore.storeSession(createdSession).block();
|
||||
|
||||
verify(this.sessionRepository).save(this.createSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveSessionThenStarted() {
|
||||
String id = "id";
|
||||
@@ -275,9 +286,9 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void setClockWhenNullThenException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.webSessionStore.setClock(null))
|
||||
.withMessage("clock cannot be null");
|
||||
assertThatThrownBy(() -> this.webSessionStore.setClock(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("clock cannot be null");
|
||||
}
|
||||
|
||||
@Test // gh-1114
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -56,9 +56,9 @@ public class WebSocketConnectHandlerDecoratorFactoryTests {
|
||||
|
||||
@Test
|
||||
public void constructorNullEventPublisher() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new WebSocketConnectHandlerDecoratorFactory(null))
|
||||
.withMessage("eventPublisher cannot be null");
|
||||
assertThatThrownBy(() -> new WebSocketConnectHandlerDecoratorFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("eventPublisher cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
@@ -82,9 +82,9 @@ public class SessionRepositoryMessageInterceptorTests {
|
||||
|
||||
@Test
|
||||
public void preSendconstructorNullRepository() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new SessionRepositoryMessageInterceptor<>(null))
|
||||
.withMessage("sessionRepository cannot be null");
|
||||
assertThatThrownBy(() -> new SessionRepositoryMessageInterceptor<>(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("sessionRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,16 +134,17 @@ public class SessionRepositoryMessageInterceptorTests {
|
||||
|
||||
@Test
|
||||
public void setMatchingMessageTypesNull() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.interceptor.setMatchingMessageTypes(null))
|
||||
.withMessage("matchingMessageTypes cannot be null or empty");
|
||||
assertThatThrownBy(() -> this.interceptor.setMatchingMessageTypes(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("matchingMessageTypes cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMatchingMessageTypesEmpty() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(
|
||||
assertThatThrownBy(
|
||||
() -> this.interceptor.setMatchingMessageTypes(Collections.emptySet()))
|
||||
.withMessage("matchingMessageTypes cannot be null or empty");
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("matchingMessageTypes cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
*/
|
||||
public abstract class AbstractRedisITests {
|
||||
|
||||
private static final String DOCKER_IMAGE = "redis:5.0.4";
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.12";
|
||||
|
||||
protected static class BaseConfig {
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveRedisOperationsSessionRepository}.
|
||||
@@ -210,10 +209,7 @@ public class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedi
|
||||
this.repository.save(session).block();
|
||||
|
||||
toSave.setLastAccessedTime(Instant.now());
|
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class)
|
||||
.isThrownBy(() -> this.repository.save(toSave).block())
|
||||
.withMessage("Session was invalidated");
|
||||
this.repository.save(toSave).block();
|
||||
|
||||
assertThat(this.repository.findById(sessionId).block()).isNull();
|
||||
assertThat(this.repository.findById(session.getId()).block()).isNotNull();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -118,15 +118,6 @@ public class ReactiveRedisOperationsSessionRepository implements
|
||||
this.redisFlushMode = redisFlushMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(() -> {
|
||||
@@ -156,9 +147,7 @@ public class ReactiveRedisOperationsSessionRepository implements
|
||||
session.hasChangedSessionId() ? session.originalSessionId
|
||||
: session.getId());
|
||||
return this.sessionRedisOperations.hasKey(sessionKey)
|
||||
.flatMap((exists) -> exists ? result
|
||||
: Mono.error(new IllegalStateException(
|
||||
"Session was invalidated")));
|
||||
.flatMap((exists) -> exists ? result : Mono.empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ public class RedisOperationsSessionRepository implements
|
||||
|
||||
@Override
|
||||
public void save(RedisSession session) {
|
||||
session.save();
|
||||
session.saveDelta();
|
||||
if (session.isNew()) {
|
||||
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
|
||||
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
|
||||
@@ -516,13 +516,12 @@ public class RedisOperationsSessionRepository implements
|
||||
|
||||
@Override
|
||||
public RedisSession createSession() {
|
||||
Duration maxInactiveInterval = Duration
|
||||
.ofSeconds((this.defaultMaxInactiveInterval != null)
|
||||
? this.defaultMaxInactiveInterval
|
||||
: MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
|
||||
RedisSession session = new RedisSession(maxInactiveInterval);
|
||||
session.flushImmediateIfNecessary();
|
||||
return session;
|
||||
RedisSession redisSession = new RedisSession();
|
||||
if (this.defaultMaxInactiveInterval != null) {
|
||||
redisSession.setMaxInactiveInterval(
|
||||
Duration.ofSeconds(this.defaultMaxInactiveInterval));
|
||||
}
|
||||
return redisSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -712,17 +711,14 @@ public class RedisOperationsSessionRepository implements
|
||||
/**
|
||||
* Creates a new instance ensuring to mark all of the new attributes to be
|
||||
* persisted in the next save operation.
|
||||
*
|
||||
* @param maxInactiveInterval the amount of time that the {@link Session} should
|
||||
* be kept alive between client requests.
|
||||
*/
|
||||
RedisSession(Duration maxInactiveInterval) {
|
||||
RedisSession() {
|
||||
this(new MapSession());
|
||||
this.cached.setMaxInactiveInterval(maxInactiveInterval);
|
||||
this.delta.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli());
|
||||
this.delta.put(MAX_INACTIVE_ATTR, (int) getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime().toEpochMilli());
|
||||
this.isNew = true;
|
||||
this.flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,7 +808,7 @@ public class RedisOperationsSessionRepository implements
|
||||
|
||||
private void flushImmediateIfNecessary() {
|
||||
if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) {
|
||||
save();
|
||||
saveDelta();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,20 +817,16 @@ public class RedisOperationsSessionRepository implements
|
||||
this.flushImmediateIfNecessary();
|
||||
}
|
||||
|
||||
private void save() {
|
||||
saveChangeSessionId();
|
||||
saveDelta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves any attributes that have been changed and updates the expiration of this
|
||||
* session.
|
||||
*/
|
||||
private void saveDelta() {
|
||||
String sessionId = getId();
|
||||
saveChangeSessionId(sessionId);
|
||||
if (this.delta.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String sessionId = getId();
|
||||
getSessionBoundHashOperations(sessionId).putAll(this.delta);
|
||||
String principalSessionKey = getSessionAttrNameKey(
|
||||
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
|
||||
@@ -867,32 +859,30 @@ public class RedisOperationsSessionRepository implements
|
||||
.onExpirationUpdated(originalExpiration, this);
|
||||
}
|
||||
|
||||
private void saveChangeSessionId() {
|
||||
String sessionId = getId();
|
||||
if (sessionId.equals(this.originalSessionId)) {
|
||||
return;
|
||||
private void saveChangeSessionId(String sessionId) {
|
||||
if (!sessionId.equals(this.originalSessionId)) {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
* Copyright 2014-2017 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.
|
||||
@@ -23,14 +23,14 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
|
||||
/**
|
||||
* Annotation used to inject the Redis accessor used by Spring Session's Redis session
|
||||
* repository.
|
||||
* Annotation used to inject the {@link RedisOperations} instance used by Spring Session's
|
||||
* {@link RedisOperationsSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @see org.springframework.session.data.redis.RedisOperationsSessionRepository#getSessionRedisOperations()
|
||||
* @see org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository#getSessionRedisOperations()
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.Map;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -65,8 +64,6 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
private ReactiveRedisConnectionFactory redisConnectionFactory;
|
||||
|
||||
private RedisSerializer<Object> defaultRedisSerializer;
|
||||
|
||||
private ClassLoader classLoader;
|
||||
|
||||
private StringValueResolver embeddedValueResolver;
|
||||
@@ -110,13 +107,6 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
this.redisConnectionFactory = redisConnectionFactoryToUse;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@Qualifier("springSessionDefaultRedisSerializer")
|
||||
public void setDefaultRedisSerializer(
|
||||
RedisSerializer<Object> defaultRedisSerializer) {
|
||||
this.defaultRedisSerializer = defaultRedisSerializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
@@ -144,11 +134,10 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
|
||||
|
||||
private ReactiveRedisTemplate<String, Object> createReactiveRedisTemplate() {
|
||||
RedisSerializer<String> keySerializer = new StringRedisSerializer();
|
||||
RedisSerializer<Object> defaultSerializer = (this.defaultRedisSerializer != null)
|
||||
? this.defaultRedisSerializer
|
||||
: new JdkSerializationRedisSerializer(this.classLoader);
|
||||
RedisSerializer<Object> valueSerializer = new JdkSerializationRedisSerializer(
|
||||
this.classLoader);
|
||||
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
|
||||
.<String, Object>newSerializationContext(defaultSerializer)
|
||||
.<String, Object>newSerializationContext(valueSerializer)
|
||||
.key(keySerializer).hashKey(keySerializer).build();
|
||||
return new ReactiveRedisTemplate<>(this.redisConnectionFactory,
|
||||
serializationContext);
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepo
|
||||
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.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
@@ -80,9 +80,9 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void constructorWithNullReactiveRedisOperations() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
|
||||
.withMessageContaining("sessionRedisOperations cannot be null");
|
||||
assertThatThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("sessionRedisOperations cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,16 +95,16 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void nullRedisKeyNamespace() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.repository.setRedisKeyNamespace(null))
|
||||
.withMessage("namespace cannot be null or empty");
|
||||
assertThatThrownBy(() -> this.repository.setRedisKeyNamespace(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("namespace cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyRedisKeyNamespace() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.repository.setRedisKeyNamespace(""))
|
||||
.withMessage("namespace cannot be null or empty");
|
||||
assertThatThrownBy(() -> this.repository.setRedisKeyNamespace(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("namespace cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -125,9 +125,9 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void nullRedisFlushMode() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.repository.setRedisFlushMode(null))
|
||||
.withMessage("redisFlushMode cannot be null");
|
||||
assertThatThrownBy(() -> this.repository.setRedisFlushMode(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redisFlushMode cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -346,16 +346,12 @@ public class ReactiveRedisOperationsSessionRepositoryTests {
|
||||
.isEqualTo(expected.getAttribute(attribute1));
|
||||
assertThat(session.<String>getAttribute(attribute2))
|
||||
.isEqualTo(expected.getAttribute(attribute2));
|
||||
assertThat(session.getCreationTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(expected.getCreationTime()
|
||||
.truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(session.getMaxInactiveInterval())
|
||||
assertThat(session.getCreationTime()).isEqualTo(expected.getCreationTime());
|
||||
assertThat(session.getMaxInactiveInterval())
|
||||
.isEqualTo(expected.getMaxInactiveInterval());
|
||||
assertThat(
|
||||
session.getLastAccessedTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(expected.getLastAccessedTime()
|
||||
.truncatedTo(ChronoUnit.MILLIS));
|
||||
}).verifyComplete();
|
||||
assertThat(session.getLastAccessedTime())
|
||||
.isEqualTo(expected.getLastAccessedTime());
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 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.
|
||||
@@ -59,7 +59,7 @@ import org.springframework.session.data.redis.RedisOperationsSessionRepository.R
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -115,9 +115,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void setApplicationEventPublisherNull() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.redisRepository.setApplicationEventPublisher(null))
|
||||
.withMessage("applicationEventPublisher cannot be null");
|
||||
assertThatThrownBy(() -> this.redisRepository.setApplicationEventPublisher(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("applicationEventPublisher cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -343,8 +343,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void redisSessionGetAttributes() {
|
||||
String attrName = "attrName";
|
||||
RedisSession session = this.redisRepository.new RedisSession(
|
||||
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
|
||||
RedisSession session = this.redisRepository.new RedisSession();
|
||||
assertThat(session.getAttributeNames()).isEmpty();
|
||||
session.setAttribute(attrName, "attrValue");
|
||||
assertThat(session.getAttributeNames()).containsOnly(attrName);
|
||||
@@ -433,12 +432,12 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
.isEqualTo(expected.getAttribute(attribute1));
|
||||
assertThat(session.<String>getAttribute(attribute2))
|
||||
.isEqualTo(expected.getAttribute(attribute2));
|
||||
assertThat(session.getCreationTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(expected.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(session.getCreationTime()).isEqualTo(expected.getCreationTime());
|
||||
assertThat(session.getMaxInactiveInterval())
|
||||
.isEqualTo(expected.getMaxInactiveInterval());
|
||||
assertThat(session.getLastAccessedTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(expected.getLastAccessedTime().truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(session.getLastAccessedTime())
|
||||
.isEqualTo(expected.getLastAccessedTime());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -499,11 +498,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
RedisSession session = sessionIdToSessions.get(sessionId);
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getId()).isEqualTo(sessionId);
|
||||
assertThat(session.getLastAccessedTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(lastAccessed.truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(lastAccessed);
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(maxInactive);
|
||||
assertThat(session.getCreationTime().truncatedTo(ChronoUnit.MILLIS))
|
||||
.isEqualTo(createdTime.truncatedTo(ChronoUnit.MILLIS));
|
||||
assertThat(session.getCreationTime()).isEqualTo(createdTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -758,23 +755,6 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
.isEqualTo(session.getCreationTime().toEpochMilli());
|
||||
}
|
||||
|
||||
@Test // gh-1409
|
||||
public void flushModeImmediateCreateWithCustomMaxInactiveInterval() {
|
||||
given(this.redisOperations.boundHashOps(anyString()))
|
||||
.willReturn(this.boundHashOperations);
|
||||
given(this.redisOperations.boundSetOps(anyString()))
|
||||
.willReturn(this.boundSetOperations);
|
||||
given(this.redisOperations.boundValueOps(anyString()))
|
||||
.willReturn(this.boundValueOperations);
|
||||
this.redisRepository.setDefaultMaxInactiveInterval(60);
|
||||
this.redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
|
||||
this.redisRepository.createSession();
|
||||
Map<String, Object> delta = getDelta();
|
||||
assertThat(delta.size()).isEqualTo(3);
|
||||
assertThat(delta.get(RedisOperationsSessionRepository.MAX_INACTIVE_ATTR))
|
||||
.isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flushModeImmediateSetAttribute() {
|
||||
given(this.redisOperations.boundHashOps(anyString()))
|
||||
@@ -859,9 +839,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void setRedisFlushModeNull() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.redisRepository.setRedisFlushMode(null))
|
||||
.withMessage("redisFlushMode cannot be null");
|
||||
assertThatThrownBy(() -> this.redisRepository.setRedisFlushMode(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redisFlushMode cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -886,16 +866,16 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void setRedisKeyNamespaceNullNamespace() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.redisRepository.setRedisKeyNamespace(null))
|
||||
.withMessage("namespace cannot be null or empty");
|
||||
assertThatThrownBy(() -> this.redisRepository.setRedisKeyNamespace(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("namespace cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRedisKeyNamespaceEmptyNamespace() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.redisRepository.setRedisKeyNamespace(" "))
|
||||
.withMessage("namespace cannot be null or empty");
|
||||
assertThatThrownBy(() -> this.redisRepository.setRedisKeyNamespace(" "))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("namespace cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test // gh-1120
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.springframework.session.data.redis.config.annotation.SpringSessionRed
|
||||
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.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -184,10 +184,10 @@ public class RedisHttpSessionConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void multipleConnectionFactoryRedisConfig() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> registerAndRefresh(RedisConfig.class,
|
||||
MultipleConnectionFactoryRedisConfig.class))
|
||||
.withMessageContaining("expected single matching bean but found 2");
|
||||
assertThatThrownBy(() -> registerAndRefresh(RedisConfig.class,
|
||||
MultipleConnectionFactoryRedisConfig.class))
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
private void registerAndRefresh(Class<?>... annotatedClasses) {
|
||||
|
||||
@@ -27,16 +27,13 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisOperations;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
|
||||
import org.springframework.session.data.redis.RedisFlushMode;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
|
||||
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
|
||||
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.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -73,22 +70,6 @@ public class RedisWebSessionConfigurationTests {
|
||||
assertThat(repository).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void springSessionRedisOperationsResolvingConfiguration() {
|
||||
registerAndRefresh(RedisConfig.class,
|
||||
SpringSessionRedisOperationsResolvingConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
assertThat(repository).isNotNull();
|
||||
ReactiveRedisOperations<String, Object> springSessionRedisOperations = this.context
|
||||
.getBean(SpringSessionRedisOperationsResolvingConfig.class)
|
||||
.getSpringSessionRedisOperations();
|
||||
assertThat(springSessionRedisOperations).isNotNull();
|
||||
assertThat((ReactiveRedisOperations) ReflectionTestUtils.getField(repository,
|
||||
"sessionRedisOperations")).isEqualTo(springSessionRedisOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customNamespace() {
|
||||
registerAndRefresh(RedisConfig.class, CustomNamespaceConfig.class);
|
||||
@@ -194,40 +175,10 @@ public class RedisWebSessionConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void multipleConnectionFactoryRedisConfig() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> registerAndRefresh(RedisConfig.class,
|
||||
MultipleConnectionFactoryRedisConfig.class))
|
||||
.withMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void customRedisSerializerConfig() {
|
||||
registerAndRefresh(RedisConfig.class, CustomRedisSerializerConfig.class);
|
||||
|
||||
ReactiveRedisOperationsSessionRepository repository = this.context
|
||||
.getBean(ReactiveRedisOperationsSessionRepository.class);
|
||||
RedisSerializer<Object> redisSerializer = this.context
|
||||
.getBean("springSessionDefaultRedisSerializer", RedisSerializer.class);
|
||||
assertThat(repository).isNotNull();
|
||||
assertThat(redisSerializer).isNotNull();
|
||||
ReactiveRedisOperations redisOperations = (ReactiveRedisOperations) ReflectionTestUtils
|
||||
.getField(repository, "sessionRedisOperations");
|
||||
assertThat(redisOperations).isNotNull();
|
||||
RedisSerializationContext serializationContext = redisOperations
|
||||
.getSerializationContext();
|
||||
assertThat(ReflectionTestUtils.getField(
|
||||
serializationContext.getValueSerializationPair().getReader(),
|
||||
"serializer")).isEqualTo(redisSerializer);
|
||||
assertThat(ReflectionTestUtils.getField(
|
||||
serializationContext.getValueSerializationPair().getWriter(),
|
||||
"serializer")).isEqualTo(redisSerializer);
|
||||
assertThat(ReflectionTestUtils.getField(
|
||||
serializationContext.getHashValueSerializationPair().getReader(),
|
||||
"serializer")).isEqualTo(redisSerializer);
|
||||
assertThat(ReflectionTestUtils.getField(
|
||||
serializationContext.getHashValueSerializationPair().getWriter(),
|
||||
"serializer")).isEqualTo(redisSerializer);
|
||||
assertThatThrownBy(() -> registerAndRefresh(RedisConfig.class,
|
||||
MultipleConnectionFactoryRedisConfig.class))
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
private void registerAndRefresh(Class<?>... annotatedClasses) {
|
||||
@@ -250,18 +201,6 @@ public class RedisWebSessionConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableRedisWebSession
|
||||
static class SpringSessionRedisOperationsResolvingConfig {
|
||||
|
||||
@SpringSessionRedisOperations
|
||||
private ReactiveRedisOperations<String, Object> springSessionRedisOperations;
|
||||
|
||||
public ReactiveRedisOperations<String, Object> getSpringSessionRedisOperations() {
|
||||
return this.springSessionRedisOperations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableRedisWebSession(redisNamespace = REDIS_NAMESPACE)
|
||||
static class CustomNamespaceConfig {
|
||||
|
||||
@@ -336,15 +275,4 @@ public class RedisWebSessionConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableRedisWebSession
|
||||
static class CustomRedisSerializerConfig {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
|
||||
return mock(RedisSerializer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ apply plugin: 'io.spring.convention.spring-module'
|
||||
dependencies {
|
||||
compile project(':spring-session-core')
|
||||
compile "com.hazelcast:hazelcast"
|
||||
compile "javax.annotation:javax.annotation-api"
|
||||
compile "org.springframework:spring-context"
|
||||
|
||||
testCompile "javax.servlet:javax.servlet-api"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 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.
|
||||
@@ -48,7 +48,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
public class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
|
||||
|
||||
private static GenericContainer container = new GenericContainer<>(
|
||||
"hazelcast/hazelcast:3.11.4")
|
||||
"hazelcast/hazelcast:3.9.4")
|
||||
.withExposedPorts(5701)
|
||||
.withEnv("JAVA_OPTS",
|
||||
"-Dhazelcast.config=/opt/hazelcast/config_ext/hazelcast.xml")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 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.
|
||||
@@ -122,7 +122,7 @@ public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
|
||||
.isInstanceOf(SessionExpiredEvent.class);
|
||||
|
||||
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
|
||||
assertThat(this.repository.<Session>findById(sessionToSave.getId())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,18 +147,24 @@ public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void saveUpdatesTimeToLiveTest() throws InterruptedException {
|
||||
Object lock = new Object();
|
||||
|
||||
S sessionToSave = this.repository.createSession();
|
||||
sessionToSave.setMaxInactiveInterval(Duration.ofSeconds(3));
|
||||
|
||||
this.repository.save(sessionToSave);
|
||||
|
||||
Thread.sleep(2000);
|
||||
synchronized (lock) {
|
||||
lock.wait(sessionToSave.getMaxInactiveInterval().minusMillis(500).toMillis());
|
||||
}
|
||||
|
||||
// Get and save the session like SessionRepositoryFilter would.
|
||||
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
|
||||
sessionToUpdate.setLastAccessedTime(Instant.now());
|
||||
this.repository.save(sessionToUpdate);
|
||||
|
||||
Thread.sleep(2000);
|
||||
synchronized (lock) {
|
||||
lock.wait(sessionToUpdate.getMaxInactiveInterval().minusMillis(100).toMillis());
|
||||
}
|
||||
|
||||
assertThat(this.repository.findById(sessionToUpdate.getId())).isNotNull();
|
||||
}
|
||||
@@ -181,28 +187,6 @@ public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isFalse();
|
||||
}
|
||||
|
||||
@Test // gh-1300
|
||||
public void updateMaxInactiveIntervalTest() throws InterruptedException {
|
||||
S sessionToSave = this.repository.createSession();
|
||||
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
|
||||
this.repository.save(sessionToSave);
|
||||
|
||||
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
|
||||
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
|
||||
.isInstanceOf(SessionCreatedEvent.class);
|
||||
this.registry.clear();
|
||||
|
||||
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
|
||||
sessionToUpdate.setLastAccessedTime(Instant.now());
|
||||
sessionToUpdate.setMaxInactiveInterval(Duration.ofSeconds(1));
|
||||
this.repository.save(sessionToUpdate);
|
||||
|
||||
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
|
||||
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
|
||||
.isInstanceOf(SessionExpiredEvent.class);
|
||||
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
|
||||
static class HazelcastSessionConfig {
|
||||
@@ -216,7 +200,6 @@ public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
|
||||
public SessionEventRegistry sessionEventRegistry() {
|
||||
return new SessionEventRegistry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 https://www.hazelcast.com/schema/config/hazelcast-config-3.11.xsd">
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
|
||||
|
||||
<user-code-deployment enabled="true">
|
||||
<class-cache-mode>ETERNAL</class-cache-mode>
|
||||
|
||||
@@ -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 https://www.hazelcast.com/schema/config/hazelcast-config-3.11.xsd">
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
|
||||
|
||||
<group>
|
||||
<name>spring-session-it-test-idle-time-map-name</name>
|
||||
|
||||
@@ -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 https://www.hazelcast.com/schema/config/hazelcast-config-3.11.xsd">
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.9.xsd">
|
||||
|
||||
<group>
|
||||
<name>spring-session-it-test-map-name</name>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ryuk.container.timeout=120
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 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.
|
||||
@@ -48,7 +48,6 @@ 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
|
||||
@@ -121,9 +120,6 @@ public class HazelcastSessionRepository implements
|
||||
*/
|
||||
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
|
||||
|
||||
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;
|
||||
@@ -242,9 +238,6 @@ public class HazelcastSessionRepository implements
|
||||
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
|
||||
}
|
||||
if (session.maxInactiveIntervalChanged) {
|
||||
if (SUPPORTS_SET_TTL) {
|
||||
updateTtl(session);
|
||||
}
|
||||
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
|
||||
}
|
||||
if (!session.delta.isEmpty()) {
|
||||
@@ -255,11 +248,6 @@ public class HazelcastSessionRepository implements
|
||||
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);
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
import com.hazelcast.core.Offloadable;
|
||||
import com.hazelcast.map.AbstractEntryProcessor;
|
||||
import com.hazelcast.map.EntryProcessor;
|
||||
|
||||
@@ -34,7 +33,7 @@ import org.springframework.session.MapSession;
|
||||
* @see HazelcastSessionRepository#save(HazelcastSessionRepository.HazelcastSession)
|
||||
*/
|
||||
public class SessionUpdateEntryProcessor
|
||||
extends AbstractEntryProcessor<String, MapSession> implements Offloadable {
|
||||
extends AbstractEntryProcessor<String, MapSession> {
|
||||
|
||||
private Instant lastAccessedTime;
|
||||
|
||||
@@ -68,11 +67,6 @@ public class SessionUpdateEntryProcessor
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutorName() {
|
||||
return OFFLOADABLE_EXECUTOR;
|
||||
}
|
||||
|
||||
void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.lastAccessedTime = lastAccessedTime;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 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.
|
||||
@@ -39,10 +39,9 @@ import org.springframework.session.MapSession;
|
||||
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
@@ -79,9 +78,9 @@ public class HazelcastSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void constructorNullHazelcastInstance() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> new HazelcastSessionRepository(null))
|
||||
.withMessage("HazelcastInstance must not be null");
|
||||
assertThatThrownBy(() -> new HazelcastSessionRepository(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("HazelcastInstance must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -260,12 +259,10 @@ public class HazelcastSessionRepositoryTests {
|
||||
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
|
||||
|
||||
HazelcastSession session = this.repository.createSession();
|
||||
String sessionId = session.getId();
|
||||
session.setMaxInactiveInterval(Duration.ofSeconds(1));
|
||||
verify(this.sessions, times(1)).set(eq(sessionId),
|
||||
verify(this.sessions, times(1)).set(eq(session.getId()),
|
||||
eq(session.getDelegate()), isA(Long.class), eq(TimeUnit.SECONDS));
|
||||
verify(this.sessions).setTtl(eq(sessionId), anyLong(), any());
|
||||
verify(this.sessions, times(1)).executeOnKey(eq(sessionId),
|
||||
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()),
|
||||
any(EntryProcessor.class));
|
||||
|
||||
this.repository.save(session);
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.session.hazelcast.config.annotation.SpringSessionHaze
|
||||
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.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -62,10 +62,10 @@ public class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void noHazelcastInstanceConfiguration() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(
|
||||
() -> registerAndRefresh(NoHazelcastInstanceConfiguration.class))
|
||||
.withMessageContaining("HazelcastInstance");
|
||||
assertThatThrownBy(
|
||||
() -> registerAndRefresh(NoHazelcastInstanceConfiguration.class))
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasMessageContaining("HazelcastInstance");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -206,9 +206,10 @@ public class HazelcastHttpSessionConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void multipleHazelcastInstanceConfiguration() {
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
|
||||
assertThatThrownBy(
|
||||
() -> registerAndRefresh(MultipleHazelcastInstanceConfiguration.class))
|
||||
.withMessageContaining("expected single matching bean but found 2");
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasMessageContaining("expected single matching bean but found 2");
|
||||
}
|
||||
|
||||
private void registerAndRefresh(Class<?>... annotatedClasses) {
|
||||
|
||||
@@ -13,7 +13,6 @@ dependencies {
|
||||
|
||||
integrationTestCompile "com.h2database:h2"
|
||||
integrationTestCompile "com.microsoft.sqlserver:mssql-jdbc"
|
||||
integrationTestCompile "com.zaxxer:HikariCP"
|
||||
integrationTestCompile "mysql:mysql-connector-java"
|
||||
integrationTestCompile "org.apache.derby:derby"
|
||||
integrationTestCompile "org.hsqldb:hsqldb"
|
||||
@@ -22,6 +21,5 @@ dependencies {
|
||||
integrationTestCompile "org.testcontainers:mariadb"
|
||||
integrationTestCompile "org.testcontainers:mssqlserver"
|
||||
integrationTestCompile "org.testcontainers:mysql"
|
||||
integrationTestCompile "org.testcontainers:oracle-xe"
|
||||
integrationTestCompile "org.testcontainers:postgresql"
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 javax.sql.DataSource;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.testcontainers.containers.JdbcDatabaseContainer;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
|
||||
import org.springframework.jdbc.datasource.init.DatabasePopulator;
|
||||
|
||||
/**
|
||||
* Abstract base class for Testcontainers based {@link JdbcOperationsSessionRepository}
|
||||
* integration tests.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
public abstract class AbstractContainerJdbcOperationsSessionRepositoryITests
|
||||
extends AbstractJdbcOperationsSessionRepositoryITests {
|
||||
|
||||
static class BaseContainerConfig extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public HikariDataSource dataSource(JdbcDatabaseContainer databaseContainer) {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(databaseContainer.getJdbcUrl());
|
||||
dataSource.setUsername(databaseContainer.getUsername());
|
||||
dataSource.setPassword(databaseContainer.getPassword());
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataSourceInitializer dataSourceInitializer(DataSource dataSource,
|
||||
DatabasePopulator databasePopulator) {
|
||||
DataSourceInitializer initializer = new DataSourceInitializer();
|
||||
initializer.setDataSource(dataSource);
|
||||
initializer.setDatabasePopulator(databasePopulator);
|
||||
return initializer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user