Compare commits

..

83 Commits

Author SHA1 Message Date
Rob Winch
c14fdb283d Release 2.0.0.M4 2017-09-13 18:04:06 -05:00
Rob Winch
ee1ff3ed3b Update Spring Security 5.0.0.M4 2017-09-13 18:03:33 -05:00
Vedran Pavic
eb7bcc5eeb Harmonize ReactorSessionRepository API
This commit renames the `ReactorSessionRepository#delete` to `deleteById` in order to make API consistent with `SessionRepository`.
2017-09-12 23:00:16 +02:00
Vedran Pavic
188e5ba4e0 Optimize JDBC session cleanup SQL statement
This commit improves session cleanup handling in  `JdbcOperationsSessionRepository#cleanUpExpiredSessions` by optimizing the used SQL statement. This is done by calculating the session expiry time when persisting the session, which in turn allows the cleanup SQL statement to be more index-friendly.

Closes gh-847
2017-09-12 15:41:10 -05:00
Vedran Pavic
1e46630467 Remove MapReactorSessionRepository default constructor
This commit removes the default `MapReactorSessionRepository` so that the users are required to explicitly supply the `Map` used to store the sessions.
2017-09-12 15:22:29 -05:00
Vedran Pavic
b72c600884 Upgrade dependencies to current milestones
- Reactor Bismuth-M4
- Spring Framework 5.0.0.RC4
- Spring Data Kay-RC3
- Lettuce 5.0.0.RC2
2017-09-12 07:54:14 +02:00
Vedran Pavic
274aec1691 Fix Boot based samples 2017-09-11 13:57:38 +02:00
Rob Winch
52ea98b4ce SpringWebSessionConfigurationTests close ApplicationContext 2017-09-07 20:12:45 -05:00
Rob Winch
5c294ae1d2 Polish 2017-09-07 20:12:45 -05:00
Greg Turnquist
1752928d96 Configure WebSessionManager's WebSessionIdResolver by bean definition
Allow a WebSessionIdResolver registered as a Spring bean to be wired into the WebSessionManager.
2017-09-07 20:12:45 -05:00
Vedran Pavic
0cdee25405 Remove MapSessionRepository default constructor
This commit removes the default `MapSessionRepository` constructor so that the users are required to explicitly supply the `Map` used to store the sessions.
2017-09-07 19:58:22 -05:00
Rob Winch
4a9f1700d5 Polish
Reorder methods
2017-09-06 15:12:13 -05:00
Rob Winch
36ab358d24 Remove SpringSessionWebSessionManager
Spring's DefaultWebSessionManager now supports all the functionality
that is needed for Spring Session, so we only need to implement
WebSessionStore
2017-09-06 14:50:48 -05:00
Vedran Pavic
8e3371aed9 Allow easier customization of cookie max age logic 2017-08-24 16:00:59 -05:00
mikemassa84
2161f966de Update grails3.adoc
Add a note about spring-session and grails flash scope, with link to stackoverflow answer.
2017-08-24 14:37:32 -05:00
Vedran Pavic
63b67a501d Update guides for Redis based samples 2017-08-24 14:17:10 -05:00
Vedran Pavic
2b0431eae4 Use TestContainers for Gretty integration tests 2017-08-24 14:17:10 -05:00
Vedran Pavic
04ec086014 Use TestContainers for integration tests 2017-08-24 14:17:10 -05:00
Rob Winch
5697f49a71 Config->HelloWebfluxSessionConfig
Use a more meaningful name
2017-08-24 13:54:21 -05:00
Rob Winch
dfce66383f webflux sample uses @EnableSpringWebSession
Issue gh-861
2017-08-24 13:52:52 -05:00
Rob Winch
a83e59bf52 Polish
Fix checkstyle

Issue gh-861
2017-08-24 13:52:24 -05:00
Greg Turnquist
8b233e84ef Create @EnableSpringWebSession annotation. 2017-08-24 13:41:42 -05:00
Greg Turnquist
84e7fbace1 Fix MapReactorSessionRepository's delete(). 2017-08-24 13:40:57 -05:00
Rob Winch
f455df3333 Add WebFlux sample
Fixesh gh-857
2017-08-18 16:28:46 -05:00
Rob Winch
a7bb9d3b31 SpringSessionWebSessionManager writes on commit
when the ServerHttpResonse is commited the cookie and the session are written

Fixes gh-856
2017-08-18 16:28:46 -05:00
Kanjie Lu
5f0e4c3b85 fix typo
change "they key" to  "the key"
2017-08-05 20:39:08 -05:00
Vedran Pavic
23c6c7cf31 Upgrade Spring Boot to 2.0.0.M3
Closes gh-841
2017-07-27 14:08:48 +02:00
Vedran Pavic
c8c5fae678 Polish build 2017-07-26 08:49:45 +02:00
Vedran Pavic
f4a58622e4 Upgrade dependencies to latest snapshots
- Reactor Bismuth-BUILD-SNAPSHOT
- Spring Framework 5.0.0.BUILD-SNAPSHOT
- Spring Data Kay-BUILD-SNAPSHOT
- Spring Security 5.0.0.BUILD-SNAPSHOT
2017-07-26 08:47:23 +02:00
Joe Grandja
5384764021 Next development version 2017-07-25 10:12:45 -04:00
Joe Grandja
56033a9b68 Release version 2.0.0.M3 2017-07-25 09:31:12 -04:00
Vedran Pavic
99a2b079ac Upgrade dependencies to current milestones
- Spring Framework 5.0.0.RC3
- Spring Data Kay-RC1
- Spring Security 5.0.0.M3
2017-07-25 10:11:26 +02:00
Vedran Pavic
9120151692 Polish "Add WebFlux Support"
Closes gh-683
2017-07-21 15:31:45 +02:00
Rob Winch
5abbe66b1d Add WebFlux Support 2017-07-21 15:30:19 +02:00
Rob Winch
f00c196430 Update Dependencies to Prepare Release 2017-07-20 22:24:35 -05:00
Rob Winch
be2604ca69 Add Session.changeSessionId 2017-07-20 16:31:38 -05:00
Rob Winch
2aa71ffb6d Update to lettuce 5.0.0.RC1 2017-07-20 09:54:53 -05:00
Rob Winch
8bdcba6e50 Spring Session 2.0.0.M2 w/ Boot Samples
This is necessary because the current version of Spring Session is not
compatible with Boot 2.0.0.M2 and we cannot release against the SNAPSHOTs

Issue gh-833
2017-07-18 20:09:21 -05:00
Mark Paluch
8dd1a10f1b Adapt to changes in Spring Data Redis API 2017-07-18 16:19:05 +02:00
Vedran Pavic
1d247aa96f Align Checkstyle config with spring-build-conventions 2017-07-12 08:24:09 +02:00
Vedran Pavic
c00d6a7bf2 Apply correct convention plugin for modules 2017-07-10 09:10:33 +02:00
Vedran Pavic
c0df3bf28b Fix deprecation warnings 2017-07-10 07:28:09 +02:00
Vedran Pavic
1b8c9838a4 Fix unchecked operations build warning 2017-07-10 07:27:27 +02:00
Vedran Pavic
8a1b454121 Fix javadoc build warning 2017-07-10 07:26:35 +02:00
Vedran Pavic
ef69c8169a Polish dependencies
This commit removes needless dependency exclusions.

See gh-824
2017-07-10 07:20:35 +02:00
Rob Winch
40b3d07224 Revert "Add --debug to Jenkinsfile"
This reverts commit 9c4e20f074.
2017-07-07 11:23:54 -05:00
Rob Winch
8c726f2215 Use Gradle 3.5.1
This works around a bug in 4.0 where Gradle is hanging when trying to
resolve a configuration.
2017-07-07 11:17:32 -05:00
Rob Winch
c2a86a27ce Travis skip install 2017-07-07 10:16:13 -05:00
Rob Winch
6a08ef6f97 Polish travis build 2017-07-07 10:03:54 -05:00
Rob Winch
9c4e20f074 Add --debug to Jenkinsfile
Try and troubleshoot why the build is hanging
2017-07-07 09:42:24 -05:00
Vedran Pavic
5845a9c46a Improve dependency management
This commit improves dependency management with the following changes:

 - `spring-session-core`: move `javax.servlet-api` from `provided` to `optional` configuration due to introduction of reactive support
 - `spring-session-data-redis`: remove Redis driver from `compile` configuration
 - Boot samples: delegate Redis driver choice to `spring-boot-starter-data-redis`
 - polish `test` configuration dependencies
2017-07-06 17:05:01 -05:00
Rob Winch
7c6693a268 Remove Sonar to see if it fixes build 2017-07-06 08:49:21 -05:00
Vedran Pavic
05a3f59813 Simplify Hazelcast sample 2017-07-05 16:00:28 -05:00
Vedran Pavic
47a7a35aa4 Remove use of Assert#notNull from core components
Fixes gh-820
2017-07-05 17:04:03 +02:00
Vedran Pavic
04b4fe3e3b Fix Checkstyle violations 2017-07-01 00:04:46 +02:00
Rob Winch
36bb65e4b5 Add default methods to Session
Fixes gh-819
2017-06-30 10:25:14 -05:00
Rob Winch
8ef36e4f3e Session Optional<T> getAttribute -> T getAttribute
Issue gh-819
2017-06-30 10:24:59 -05:00
Rob Winch
ab3e280993 Update to latest SNAPSHOTs 2017-06-29 22:20:26 -05:00
Rob Winch
30562b5749 Use Spring IO Cairo-BUILD-SNAPSHOT 2017-06-27 16:37:13 -05:00
Rob Winch
d42a7b65ea Add MapReactorSessionRepository
Fixes gh-815
2017-06-27 16:32:51 -05:00
Rob Winch
db9807d12b Add ReactorSessionRepository
Fixes gh-814
2017-06-27 16:32:51 -05:00
Rob Winch
db09fa8168 Use SPRING_SESSION_TEAM_EMAILS 2017-06-26 10:52:17 -05:00
Vedran Pavic
031541bc05 Fix Checkstyle violations 2017-06-26 08:45:20 +02:00
Vedran Pavic
084e3428fb Move SessionEventRegistry back to integration-test sources
Fixes gh-810
2017-06-23 19:03:46 +02:00
Vedran Pavic
b321ff02f0 Revert "SessionRepository.save returns S"
See gh-809
2017-06-23 18:48:30 +02:00
Rob Winch
c6c6beb40c Session.delete -> deleteById
Fixes gh-809
2017-06-22 21:30:14 -05:00
Rob Winch
0127ef9f9b SessionRepository.getSession(String) -> findById(String)
Issue gh-809
2017-06-22 21:29:34 -05:00
Rob Winch
cd8686ae9c SessionRepository.save returns S
Issue gh-809
2017-06-22 21:27:25 -05:00
Rob Winch
233d179bfa Revert "Allow Publishing When Spring IO Fails"
This reverts commit fca411996a.
2017-06-22 13:20:23 -05:00
Rob Winch
4e8ae8d9d4 Revert "Deploy Without Checks for Spring IO"
This reverts commit 8c6810c6dd.
2017-06-22 13:20:21 -05:00
Rob Winch
8c6810c6dd Deploy Without Checks for Spring IO 2017-06-22 13:18:01 -05:00
Rob Winch
fca411996a Allow Publishing When Spring IO Fails
This is necessary to solve the problem of chicken and the Egg. See
https://github.com/spring-io/platform/issues/622#issuecomment-310452646
2017-06-22 13:00:12 -05:00
Rob Winch
79b8296e1c Work Around spring-projects/spring-boot#9573
Issue gh-806
2017-06-22 09:04:23 -05:00
Rob Winch
043cb42149 Extract spring-session-jdbc
Issue gh-806
2017-06-22 09:04:09 -05:00
Rob Winch
c28f047eb5 Extract spring-session-data-hazelcast
Issue gh-806
2017-06-22 09:03:50 -05:00
Rob Winch
972cf66d7e Extract spring-session-data-redis
Issue gh-806
2017-06-22 09:03:25 -05:00
Rob Winch
f1319483ee Move spring-session to spring-session-core
Issue gh-806
2017-06-22 09:02:44 -05:00
Rob Winch
6ad5006280 Update to Gradle 4.0 2017-06-16 13:40:43 -05:00
Vedran Pavic
f7e07b7f6b Improve Session API to use Java 8 2017-06-16 11:44:19 -05:00
Vedran Pavic
4cf26d9c36 Move ExpiringSession API into Session 2017-06-16 11:44:19 -05:00
Vedran Pavic
a848df1235 Replace explicit type arguments with diamond operator 2017-06-16 11:44:19 -05:00
Rob Winch
f8292ba512 Update to Spring Boot 2.0.0.M2
Fixes gh-801
2017-06-16 10:29:30 -05:00
Rob Winch
21bcc6e8d7 Next Development Version 2017-06-15 20:35:20 -05:00
241 changed files with 4835 additions and 1493 deletions

View File

@@ -1,19 +1,20 @@
language: java
services:
- redis-server
sudo: required
jdk:
- oraclejdk8
services: docker
os:
- linux
jdk: oraclejdk8
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
script: ./gradlew build
install: true
script: ./gradlew clean build --refresh-dependencies --no-daemon

17
Jenkinsfile vendored
View File

@@ -24,21 +24,6 @@ try {
}
}
},
sonar: {
stage('Sonar') {
node {
checkout scm
withCredentials([string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')]) {
try {
sh "./gradlew clean sonarqube -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon"
} catch(Exception e) {
currentBuild.result = 'FAILED: sonar'
throw e
}
}
}
}
},
springio: {
stage('Spring IO') {
node {
@@ -95,7 +80,7 @@ try {
subject: subject,
body: details,
recipientProviders: RECIPIENTS,
to: "$SPRING_SECURITY_TEAM_EMAILS"
to: "$SPRING_SESSION_TEAM_EMAILS"
)
}
}

View File

@@ -1,11 +1,10 @@
buildscript {
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.2.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.3.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
repositories {
maven { url 'https://repo.spring.io/libs-snapshot' }
maven { url 'https://repo.spring.io/plugins-snapshot' }
maven { url 'https://repo.spring.io/plugins-release' }
}
}
apply plugin: 'io.spring.convention.root'

View File

@@ -2,8 +2,10 @@ apply plugin: 'io.spring.convention.docs'
apply plugin: 'io.spring.convention.spring-test'
dependencies {
testCompile project(':spring-session')
testCompile project(':spring-session-core')
testCompile project(':spring-session-data-redis')
testCompile project(':spring-session-hazelcast')
testCompile project(':spring-session-jdbc')
testCompile "org.springframework:spring-jdbc"
testCompile "org.springframework:spring-messaging"
testCompile "org.springframework:spring-webmvc"
@@ -39,5 +41,5 @@ asciidoctor {
'docs-test-dir' : rootProject.projectDir.path + '/docs/src/test/java/',
'docs-test-resources-dir' : rootProject.projectDir.path + '/docs/src/test/resources/',
'samples-dir' : rootProject.projectDir.path + '/samples/',
'session-main-resources-dir' : rootProject.projectDir.path + '/spring-session/src/main/resources/'
'session-jdbc-main-resources-dir' : project(':spring-session-jdbc').projectDir.path + '/src/main/resources/'
}

View File

@@ -110,6 +110,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `JedisConnectionFactory` 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.
====
----

View File

@@ -93,6 +93,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `JedisConnectionFactory` 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.
====
----

View File

@@ -85,6 +85,7 @@ server.session.timeout=60
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -86,6 +86,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `JedisConnectionFactory` 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.
====
----
@@ -127,3 +128,6 @@ Alternatively, you can also delete the explicit key. Enter the following into yo
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
Now visit the application at http://localhost:8080/test/index and observe that we are no longer authenticated.
NOTE: Spring Session will not work with grails flash scope without additional work. +
See this answer for an explanation: https://stackoverflow.com/a/43311427

View File

@@ -80,6 +80,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -127,6 +127,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -128,6 +128,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -132,6 +132,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -21,6 +21,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -135,6 +135,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `LettuceConnectionFactory` 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.
====
----

View File

@@ -416,7 +416,7 @@ include::{docs-test-dir}docs/security/SecurityConfiguration.java[tags=class]
----
This assumes that you've also configured Spring Session to provide a `FindByIndexNameSessionRepository` that
returns `ExpiringSession` instances.
returns `Session` instances.
When using XML configuration, it would look something like this:
[source,xml,indent=0]
@@ -458,11 +458,7 @@ include::{indexdoc-tests}[tags=repository-demo]
<5> We retrieve the `Session` from the `SessionRepository`.
<6> We obtain the persisted `User` from our `Session` without the need for explicitly casting our attribute.
[[api-expiringsession]]
=== ExpiringSession
An `ExpiringSession` extends a `Session` by providing attributes related to the `Session` instance's expiration.
If there is no need to interact with the expiration information, prefer using the more simple `Session` API.
`Session` API also provides attributes related to the `Session` instance's expiration.
Typical usage might look like the following:
@@ -471,17 +467,17 @@ Typical usage might look like the following:
include::{indexdoc-tests}[tags=expire-repository-demo]
----
<1> We create a `SessionRepository` instance with a generic type, `S`, that extends `ExpiringSession`. The generic type is defined in our class.
<2> We create a new `ExpiringSession` using our `SessionRepository` and assign it to a variable of type `S`.
<3> We interact with the `ExpiringSession`.
In our example, we demonstrate updating the amount of time the `ExpiringSession` can be inactive before it expires.
<4> We now save the `ExpiringSession`.
<1> We create a `SessionRepository` instance with a generic type, `S`, that extends `Session`. The generic type is defined in our class.
<2> We create a new `Session` using our `SessionRepository` and assign it to a variable of type `S`.
<3> We interact with the `Session`.
In our example, we demonstrate updating the amount of time the `Session` can be inactive before it expires.
<4> We now save the `Session`.
This is why we needed the generic type `S`.
The `SessionRepository` only allows saving `ExpiringSession` instances that were created or retrieved using the same `SessionRepository`.
The `SessionRepository` only allows saving `Session` instances that were created or retrieved using the same `SessionRepository`.
This allows for the `SessionRepository` to make implementation specific optimizations (i.e. only writing attributes that have changed).
The last accessed time is automatically updated when the `ExpiringSession` is saved.
<5> We retrieve the `ExpiringSession` from the `SessionRepository`.
If the `ExpiringSession` were expired, the result would be null.
The last accessed time is automatically updated when the `Session` is saved.
<5> We retrieve the `Session` from the `SessionRepository`.
If the `Session` were expired, the result would be null.
[[api-sessionrepository]]
=== SessionRepository
@@ -640,7 +636,7 @@ HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:a
[[api-redisoperationssessionrepository-expiration]]
===== Session Expiration
An expiration is associated to each session using the EXPIRE command based upon the `ExpiringSession.getMaxInactiveInterval()`.
An expiration is associated to each session using the EXPIRE command based upon the `Session.getMaxInactiveInterval()`.
For example:
----
@@ -653,7 +649,7 @@ An expiration is set on the session itself five minutes after it actually expire
[NOTE]
====
The `SessionRepository.getSession(String)` method ensures that no expired sessions will be returned.
The `SessionRepository.findById(String)` method ensures that no expired sessions will be returned.
This means there is no need to check the expiration before using a session.
====
@@ -670,12 +666,12 @@ EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
When a session expires key is deleted or expires, the keyspace notification triggers a lookup of the actual session and a SessionDestroyedEvent is fired.
One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if they key has not been accessed.
One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if the key has not been accessed.
Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration.
For additional details see http://redis.io/topics/notifications[Timing of expired events] section in the Redis documentation.
To circumvent the fact that expired events are not guaranteed to happen we can ensure that each key is accessed when it is expected to expire.
This means that if the TTL is expired on the key, Redis will remove the key and fire the expired event when we try to access they key.
This means that if the TTL is expired on the key, Redis will remove the key and fire the expired event when we try to access the key.
For this reason, each session expiration is also tracked to the nearest minute.
This allows a background task to access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion.
@@ -687,7 +683,7 @@ EXPIRE spring:session:expirations1439245080000 2100
----
The background task will then use these mappings to explicitly request each key.
By accessing they key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
By accessing the key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
[NOTE]
====
@@ -776,7 +772,7 @@ redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed
[[api-mapsessionrepository]]
=== MapSessionRepository
The `MapSessionRepository` allows for persisting `ExpiringSession` in a `Map` with the key being the `ExpiringSession` id and the value being the `ExpiringSession`.
The `MapSessionRepository` allows for persisting `Session` in a `Map` with the key being the `Session` id and the value being the `Session`.
The implementation can be used with a `ConcurrentHashMap` as a testing or convenience mechanism.
Alternatively, it can be used with distributed `Map` implementations. For example, it can be used with Hazelcast.
@@ -860,14 +856,14 @@ For example, with PostgreSQL database you would use the following schema script:
[source,sql,indent=0]
----
include::{session-main-resources-dir}org/springframework/session/jdbc/schema-postgresql.sql[]
include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schema-postgresql.sql[]
----
And with MySQL database:
[source,sql,indent=0]
----
include::{session-main-resources-dir}org/springframework/session/jdbc/schema-mysql.sql[]
include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schema-mysql.sql[]
----
==== Transaction management

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -21,7 +21,7 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -38,7 +38,7 @@ import static org.mockito.Mockito.mock;
@WebAppConfiguration
public class HttpSessionConfigurationNoOpConfigureRedisActionXmlTests {
@Autowired
SessionRepositoryFilter<? extends ExpiringSession> filter;
SessionRepositoryFilter<? extends Session> filter;
@Test
public void redisConnectionFactoryNotUsedSinceNoValidation() {

View File

@@ -16,6 +16,9 @@
package docs;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
@@ -26,7 +29,6 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.Session;
@@ -49,8 +51,8 @@ public class IndexDocTests {
@Test
public void repositoryDemo() {
RepositoryDemo<ExpiringSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository();
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
@@ -68,7 +70,7 @@ public class IndexDocTests {
this.repository.save(toSave); // <4>
S session = this.repository.getSession(toSave.getId()); // <5>
S session = this.repository.findById(toSave.getId()); // <5>
// <6>
User user = session.getAttribute(ATTR_USER);
@@ -81,24 +83,24 @@ public class IndexDocTests {
@Test
public void expireRepositoryDemo() {
ExpiringRepositoryDemo<ExpiringSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository();
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends ExpiringSession> {
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = this.repository.createSession(); // <2>
// ...
toSave.setMaxInactiveIntervalInSeconds(30); // <3>
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); // <3>
this.repository.save(toSave); // <4>
S session = this.repository.getSession(toSave.getId()); // <5>
S session = this.repository.findById(toSave.getId()); // <5>
// ...
}
@@ -111,7 +113,7 @@ public class IndexDocTests {
public void newRedisOperationsSessionRepository() {
// tag::new-redisoperationssessionrepository[]
LettuceConnectionFactory factory = new LettuceConnectionFactory();
SessionRepository<? extends ExpiringSession> repository = new RedisOperationsSessionRepository(
SessionRepository<? extends Session> repository = new RedisOperationsSessionRepository(
factory);
// end::new-redisoperationssessionrepository[]
}
@@ -120,7 +122,8 @@ public class IndexDocTests {
@SuppressWarnings("unused")
public void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends ExpiringSession> repository = new MapSessionRepository();
SessionRepository<? extends Session> repository = new MapSessionRepository(
new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@@ -136,7 +139,7 @@ public class IndexDocTests {
// ... configure transactionManager ...
SessionRepository<? extends ExpiringSession> repository =
SessionRepository<? extends Session> repository =
new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
// end::new-jdbcoperationssessionrepository[]
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -16,6 +16,8 @@
package docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
@@ -27,7 +29,7 @@ import org.springframework.session.config.annotation.web.http.EnableSpringHttpSe
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository();
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -16,6 +16,8 @@
package docs.http;
import java.util.Properties;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,11 +33,13 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
* @author Mark Paluch
* @since 1.2
*/
@RunWith(SpringJUnit4ClassRunner.class)
@@ -63,6 +67,7 @@ public abstract class AbstractHttpSessionListenerTests {
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);
given(connection.getConfig(anyString())).willReturn(new Properties());
return factory;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -16,6 +16,8 @@
package docs.security;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -73,7 +75,7 @@ public class RememberMeSecurityConfiguration extends WebSecurityConfigurerAdapte
@Bean
MapSessionRepository sessionRepository() {
return new MapSessionRepository();
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -16,8 +16,8 @@
package docs.security;
import java.time.Duration;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.Cookie;
@@ -26,7 +26,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
@@ -48,7 +48,7 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
@ContextConfiguration(classes = RememberMeSecurityConfiguration.class)
@WebAppConfiguration
@SuppressWarnings("rawtypes")
public class RememberMeSecurityConfigurationTests<T extends ExpiringSession> {
public class RememberMeSecurityConfigurationTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
@@ -81,9 +81,9 @@ public class RememberMeSecurityConfigurationTests<T extends ExpiringSession> {
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions
.getSession(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveIntervalInSeconds())
.isEqualTo((int) TimeUnit.DAYS.toSeconds(30));
.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofDays(30));
}
}

View File

@@ -16,8 +16,8 @@
package docs.security;
import java.time.Duration;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.Cookie;
@@ -26,7 +26,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
@@ -48,7 +48,7 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
@ContextConfiguration
@WebAppConfiguration
@SuppressWarnings("rawtypes")
public class RememberMeSecurityConfigurationXmlTests<T extends ExpiringSession> {
public class RememberMeSecurityConfigurationXmlTests<T extends Session> {
@Autowired
WebApplicationContext context;
@Autowired
@@ -81,9 +81,9 @@ public class RememberMeSecurityConfigurationXmlTests<T extends ExpiringSession>
Cookie cookie = result.getResponse().getCookie("SESSION");
assertThat(cookie.getMaxAge()).isEqualTo(Integer.MAX_VALUE);
T session = this.sessions
.getSession(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveIntervalInSeconds())
.isEqualTo((int) TimeUnit.DAYS.toSeconds(30));
.findById(new String(Base64.getDecoder().decode(cookie.getValue())));
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofDays(30));
}
}

View File

@@ -21,8 +21,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
/**
@@ -33,7 +33,7 @@ import org.springframework.session.security.SpringSessionBackedSessionRegistry;
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private FindByIndexNameSessionRepository<ExpiringSession> sessionRepository;
private FindByIndexNameSessionRepository<Session> sessionRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
@@ -48,8 +48,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry<ExpiringSession>(
this.sessionRepository);
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
}
// end::class[]

View File

@@ -24,5 +24,9 @@
</security:user-service>
<bean class="org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration"/>
<bean id="springSessionRepository" class="org.springframework.session.MapSessionRepository"/>
<bean id="springSessionRepository" class="org.springframework.session.MapSessionRepository">
<constructor-arg>
<bean class="java.util.concurrent.ConcurrentHashMap"/>
</constructor-arg>
</bean>
</beans>

View File

@@ -4,6 +4,7 @@
<suppressions>
<suppress files=".+Application\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".+Configuration\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".+SSEFluxWebConfig\.java" checks="RegexpHeader"/>
<suppress files="[\\/]src[\\/]test[\\/]java[\\/]" checks="Javadoc"/>
<suppress files="[\\/]src[\\/]integration-test[\\/]java[\\/]" checks="Javadoc"/>

View File

@@ -1,2 +1,2 @@
springBootVersion=2.0.0.M1
version=2.0.0.M2
springBootVersion=2.0.0.M3
version=2.0.0.M4

View File

@@ -1,29 +1,49 @@
dependencyManagement {
imports {
mavenBom 'org.springframework:spring-framework-bom:5.0.0.RC2'
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-M4'
mavenBom 'org.springframework.security:spring-security-bom:5.0.0.M2'
mavenBom 'io.projectreactor:reactor-bom:Bismuth-M4'
mavenBom 'org.springframework:spring-framework-bom:5.0.0.RC4'
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-RC3'
mavenBom 'org.springframework.security:spring-security-bom:5.0.0.M4'
}
dependencies {
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.0.pr3'
dependency 'com.h2database:h2:1.4.195'
dependency 'com.hazelcast:hazelcast-client:3.8'
dependency 'com.hazelcast:hazelcast:3.8'
dependencySet(group: 'com.hazelcast', version: '3.8.3') {
entry 'hazelcast'
entry 'hazelcast-client'
}
dependencySet(group: 'org.testcontainers', version: '1.4.2') {
entry 'mysql'
entry 'postgresql'
entry 'testcontainers'
}
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.0.pr4'
dependency 'com.h2database:h2:1.4.196'
dependency 'com.maxmind.geoip2:geoip2:2.3.1'
dependency 'commons-codec:commons-codec:1.10'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.0.0.M2'
dependency 'io.lettuce:lettuce-core:5.0.0.RC2'
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 'javax.servlet:javax.servlet-api:3.1.0'
dependency 'junit:junit:4.12'
dependency 'mysql:mysql-connector-java:5.1.43'
dependency 'org.apache.derby:derby:10.13.1.1'
dependency 'org.apache.httpcomponents:httpclient:4.5.3'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.assertj:assertj-core:3.6.2'
dependency 'org.assertj:assertj-core:3.8.0'
dependency 'org.hsqldb:hsqldb:2.4.0'
dependency 'org.mockito:mockito-core:2.7.22'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.26'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.0.3'
dependency 'org.mockito:mockito-core:2.8.47'
dependency 'org.postgresql:postgresql:42.1.3'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.27'
dependency 'org.slf4j:jcl-over-slf4j:1.7.25'
dependency 'org.slf4j:log4j-over-slf4j:1.7.25'
dependency 'org.testcontainers:mariadb:1.3.0'
dependency 'org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE'
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.7.RC1'
dependency 'org.webjars:bootstrap:2.3.2'
dependency 'org.webjars:html5shiv:3.7.3'
dependency 'org.webjars:jquery:1.9.0'

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Wed Jan 11 10:54:44 CST 2017
#Fri Jul 07 11:11:13 CDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip

19
gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@@ -154,16 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
exec "$JAVACMD" "$@"

View File

@@ -1,10 +1,13 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
ext['spring-data-releasetrain.version'] = 'Kay-RC3'
dependencies {
compile project(':spring-session-data-redis')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "org.springframework.boot:spring-boot-starter-data-redis"
compile "org.springframework.boot:spring-boot-devtools"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
@@ -17,12 +20,7 @@ dependencies {
testCompile "org.assertj:assertj-core"
integrationTestCompile seleniumDependencies
}
integrationTest {
doFirst {
systemProperties['spring.session.redis.namespace'] = project.name
}
integrationTestCompile "org.testcontainers:testcontainers"
}
integrationTest {

View File

@@ -18,9 +18,11 @@ package sample;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.testcontainers.containers.GenericContainer;
import sample.pages.HomePage;
import sample.pages.LoginPage;
@@ -28,6 +30,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
@@ -35,12 +41,20 @@ import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDr
/**
* @author Eddú Meléndez
* @author Rob Winch
* @author Vedran Pavic
*/
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@SpringBootTest(classes = FindByUsernameApplication.class, webEnvironment = WebEnvironment.MOCK)
@ContextConfiguration(initializers = FindByUsernameTests.Initializer.class)
public class FindByUsernameTests {
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Autowired
private MockMvc mockMvc;
@@ -72,4 +86,18 @@ public class FindByUsernameTests {
home.terminateButtonDisabled();
}
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues
.of("spring.redis.host=" + redisContainer.getContainerIpAddress(),
"spring.redis.port=" + redisContainer.getFirstMappedPort())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -21,8 +21,8 @@ import java.util.Collection;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
@@ -39,11 +39,11 @@ import org.springframework.web.bind.annotation.RequestMethod;
public class IndexController {
// tag::findbyusername[]
@Autowired
FindByIndexNameSessionRepository<? extends ExpiringSession> sessions;
FindByIndexNameSessionRepository<? extends Session> sessions;
@RequestMapping("/")
public String index(Principal principal, Model model) {
Collection<? extends ExpiringSession> usersSessions = this.sessions
Collection<? extends Session> usersSessions = this.sessions
.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal.getName())
@@ -60,7 +60,7 @@ public class IndexController {
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal.getName()).keySet();
if (usersSessionIds.contains(sessionIdToDelete)) {
this.sessions.delete(sessionIdToDelete);
this.sessions.deleteById(sessionIdToDelete);
}
return "redirect:/";

View File

@@ -21,8 +21,8 @@
<tr th:each="sessionElement : ${sessions}" th:with="details=${sessionElement.getAttribute('SESSION_DETAILS')}">
<td th:text="${sessionElement.id.substring(30)}"></td>
<td th:text="${details?.location}"></td>
<td th:text="${#dates.format(new java.util.Date(sessionElement.creationTime),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#dates.format(new java.util.Date(sessionElement.lastAccessedTime),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#temporals.format(sessionElement.creationTime.atZone(T(java.time.ZoneId).systemDefault()),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${#temporals.format(sessionElement.lastAccessedTime.atZone(T(java.time.ZoneId).systemDefault()),'dd/MMM/yyyy HH:mm:ss')}"></td>
<td th:text="${details?.accessType}"></td>
<td>
<form th:action="@{'/sessions/' + ${sessionElement.id}}" th:method="delete">

View File

@@ -1,18 +1,18 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
ext['spring-data-releasetrain.version'] = 'Kay-RC3'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "org.springframework.boot:spring-boot-starter-data-redis"
compile "org.springframework.boot:spring-boot-devtools"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
compile "org.webjars:html5shiv"
compile "org.webjars:webjars-locator"
compile "io.lettuce:lettuce-core"
compile "org.apache.httpcomponents:httpclient"
testCompile "org.springframework.boot:spring-boot-starter-test"
@@ -20,4 +20,5 @@ dependencies {
testCompile "org.skyscreamer:jsonassert"
integrationTestCompile seleniumDependencies
integrationTestCompile "org.testcontainers:testcontainers"
}

View File

@@ -20,9 +20,11 @@ import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.testcontainers.containers.GenericContainer;
import sample.pages.HomePage;
import sample.pages.HomePage.Attribute;
import sample.pages.LoginPage;
@@ -31,6 +33,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
@@ -39,12 +45,20 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eddú Meléndez
* @author Vedran Pavic
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = HttpRedisJsonTest.Initializer.class)
public class HttpRedisJsonTest {
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Autowired
private MockMvc mockMvc;
@@ -96,4 +110,18 @@ public class HttpRedisJsonTest {
assertThat(attributes).extracting("attributeValue").contains("Demo Value");
}
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues
.of("spring.redis.host=" + redisContainer.getContainerIpAddress(),
"spring.redis.port=" + redisContainer.getFirstMappedPort())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -13,28 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.testcontainers.containers.GenericContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author jitendra on 8/3/16.
* @author jitendra
* @author Vedran Pavic
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@SpringBootTest(classes = Application.class)
@ContextConfiguration(initializers = RedisSerializerTest.Initializer.class)
public class RedisSerializerTest {
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Autowired
RedisTemplate<Object, Object> sessionRedisTemplate;
private RedisTemplate<Object, Object> sessionRedisTemplate;
@Test
public void testRedisTemplate() {
@@ -43,4 +58,19 @@ public class RedisSerializerTest {
assertThat(this.sessionRedisTemplate.getDefaultSerializer())
.isInstanceOf(GenericJackson2JsonRedisSerializer.class);
}
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues
.of("spring.redis.host=" + redisContainer.getContainerIpAddress(),
"spring.redis.port=" + redisContainer.getFirstMappedPort())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}

View File

@@ -1,10 +1,13 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
ext['spring-data-releasetrain.version'] = 'Kay-RC3'
dependencies {
compile project(':spring-session-data-redis')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "org.springframework.boot:spring-boot-starter-data-redis"
compile "org.springframework.boot:spring-boot-devtools"
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
compile "org.webjars:bootstrap"
@@ -14,4 +17,5 @@ dependencies {
testCompile "org.springframework.boot:spring-boot-starter-test"
integrationTestCompile seleniumDependencies
integrationTestCompile "org.testcontainers:testcontainers"
}

View File

@@ -18,9 +18,11 @@ package sample;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.testcontainers.containers.GenericContainer;
import sample.pages.HomePage;
import sample.pages.LoginPage;
@@ -28,18 +30,30 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder;
/**
* @author Eddú Meléndez
* @author Vedran Pavic
*/
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.MOCK)
@ContextConfiguration(initializers = BootTests.Initializer.class)
public class BootTests {
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Autowired
private MockMvc mockMvc;
@@ -78,4 +92,18 @@ public class BootTests {
login.assertAt();
}
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues
.of("spring.redis.host=" + redisContainer.getContainerIpAddress(),
"spring.redis.port=" + redisContainer.getFirstMappedPort())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}

View File

@@ -1,13 +1,14 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
ext['spring-data-releasetrain.version'] = 'Kay-RC3'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
compile "org.springframework.boot:spring-boot-starter-security"
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile "org.springframework.boot:spring-boot-starter-data-redis"
compile "org.springframework.boot:spring-boot-starter-websocket"
compile "org.springframework.boot:spring-boot-devtools"
compile "org.springframework:spring-websocket"
@@ -20,9 +21,9 @@ dependencies {
compile "org.webjars:sockjs-client"
compile "org.webjars:stomp-websocket"
compile "org.webjars:webjars-locator"
compile "io.lettuce:lettuce-core"
compile "com.h2database:h2"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.springframework.security:spring-security-test"
testCompile "org.testcontainers:testcontainers"
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -19,7 +19,7 @@ package sample.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@@ -29,7 +29,7 @@ import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig
extends AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> { // <1>
extends AbstractSessionWebSocketMessageBrokerConfigurer<Session> { // <1>
protected void configureStompEndpoints(StompEndpointRegistry registry) { // <2>
registry.addEndpoint("/messages").withSockJS();

View File

@@ -23,7 +23,7 @@ import sample.websocket.WebSocketDisconnectHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
/**
* These handlers are separated from WebSocketConfig because they are specific to this
@@ -32,7 +32,7 @@ import org.springframework.session.ExpiringSession;
* @author Rob Winch
*/
@Configuration
public class WebSocketHandlersConfig<S extends ExpiringSession> {
public class WebSocketHandlersConfig<S extends Session> {
@Bean
public WebSocketConnectHandler<S> webSocketConnectHandler(

View File

@@ -20,14 +20,21 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.testcontainers.containers.GenericContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.TextMessage;
@@ -41,18 +48,27 @@ import org.springframework.web.socket.sockjs.client.WebSocketTransport;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = ApplicationTests.Initializer.class)
public class ApplicationTests {
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Value("${local.server.port}")
String port;
private String port;
@Autowired
WebSocketHandler webSocketHandler;
private WebSocketHandler webSocketHandler;
@Test
public void run() throws Exception {
@@ -67,4 +83,19 @@ public class ApplicationTests {
this.thrown.expect(ExecutionException.class);
wsSession.get().sendMessage(new TextMessage("a"));
}
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(
ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues
.of("spring.redis.host=" + redisContainer.getContainerIpAddress(),
"spring.redis.port=" + redisContainer.getFirstMappedPort())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}

View File

@@ -1,15 +1,14 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework:spring-web"
compile "io.lettuce:lettuce-core"
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-taglib"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
@@ -18,3 +17,7 @@ dependencies {
integrationTestCompile seleniumDependencies
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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,11 +17,13 @@
package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Import(EmbeddedRedisConfig.class)
@EnableRedisHttpSession
public class Config {

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -1,7 +1,7 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile project(':spring-session')
compile project(':spring-session-hazelcast')
compile "org.springframework:spring-web"
compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-web"

View File

@@ -1,15 +1,14 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework:spring-web"
compile "io.lettuce:lettuce-core"
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-taglib"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
@@ -18,3 +17,7 @@ dependencies {
integrationTestCompile seleniumDependencies
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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,9 +17,11 @@
package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Import(EmbeddedRedisConfig.class)
// tag::class[]
@EnableRedisHttpSession // <1>
public class Config {

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -1,9 +1,7 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "io.lettuce:lettuce-core"
compile "org.springframework:spring-webmvc"
compile "org.springframework.security:spring-security-config"
@@ -11,6 +9,7 @@ dependencies {
compile "com.fasterxml.jackson.core:jackson-databind"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
@@ -19,4 +18,10 @@ dependencies {
testCompile "org.assertj:assertj-core"
testCompile "org.springframework:spring-test"
testCompile "commons-codec:commons-codec"
}
integrationTestCompile "org.testcontainers:testcontainers"
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -17,16 +17,22 @@
package rest;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import sample.HttpSessionConfig;
import org.testcontainers.containers.GenericContainer;
import sample.SecurityConfig;
import sample.mvc.MvcConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.HeaderHttpSessionStrategy;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -44,18 +50,24 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { HttpSessionConfig.class, SecurityConfig.class,
@ContextConfiguration(classes = { RestMockMvcTests.Config.class, SecurityConfig.class,
MvcConfig.class })
@WebAppConfiguration
public class RestMockMvcTests {
@Autowired
SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter;
private static final String DOCKER_IMAGE = "redis:3.2.9";
@ClassRule
public static GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE)
.withExposedPorts(6379);
@Autowired
WebApplicationContext context;
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
MockMvc mvc;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
@@ -81,4 +93,21 @@ public class RestMockMvcTests {
.andExpect(content().string("{\"username\":\"user\"}"));
}
@Configuration
@EnableRedisHttpSession
static class Config {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer.getContainerIpAddress(),
redisContainer.getFirstMappedPort());
}
@Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -18,11 +18,13 @@ package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.HeaderHttpSessionStrategy;
import org.springframework.session.web.http.HttpSessionStrategy;
@Import(EmbeddedRedisConfig.class)
// tag::class[]
@Configuration
@EnableRedisHttpSession // <1>

View File

@@ -1,9 +1,7 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework:spring-web"
compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-web"
@@ -12,6 +10,7 @@ dependencies {
compile "org.webjars:webjars-taglib"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
providedCompile "javax.servlet.jsp:javax.servlet.jsp-api"
@@ -22,3 +21,7 @@ dependencies {
integrationTestCompile seleniumDependencies
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -18,9 +18,11 @@ package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Import(EmbeddedRedisConfig.class)
// tag::class[]
@Configuration
@EnableRedisHttpSession // <1>

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -1,15 +1,14 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework:spring-web"
compile "io.lettuce:lettuce-core"
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-taglib"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
@@ -19,3 +18,7 @@ dependencies {
integrationTestCompile seleniumDependencies
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -18,13 +18,14 @@ package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* @author Rob Winch
*/
@Import(EmbeddedRedisConfig.class)
// tag::class[]
@Configuration
@EnableRedisHttpSession

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -61,7 +61,7 @@ public class UserAccountsFilter implements Filter {
String alias = entry.getKey();
String sessionId = entry.getValue();
Session session = repo.getSession(sessionId);
Session session = repo.findById(sessionId);
if (session == null) {
continue;
}

View File

@@ -0,0 +1,22 @@
apply plugin: 'io.spring.convention.spring-sample'
dependencies {
compile project(':spring-session-core')
compile 'io.lettuce:lettuce-core'
compile 'io.netty:netty-buffer'
compile 'io.projectreactor.ipc:reactor-netty'
compile 'org.springframework:spring-context'
compile 'org.springframework:spring-web'
compile 'org.springframework:spring-webflux'
compile 'org.thymeleaf.extras:thymeleaf-extras-java8time'
compile 'org.thymeleaf:thymeleaf-spring5'
compile 'org.webjars:bootstrap'
compile 'org.webjars:webjars-taglib'
compile jstlDependencies
compile slf4jDependencies
testCompile 'junit:junit'
testCompile 'org.assertj:assertj-core'
integrationTestCompile seleniumDependencies
}

View File

@@ -0,0 +1,90 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import sample.pages.HomePage;
import sample.pages.HomePage.Attribute;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eddú Meléndez
* @author Rob Winch
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = HelloWebfluxApplication.class)
@TestPropertySource(properties = "server.port=0")
public class AttributeTests {
@Value("#{@nettyContext.address().getPort()}")
int port;
private WebDriver driver;
@Before
public void setup() {
this.driver = new HtmlUnitDriver();
}
@After
public void tearDown() {
this.driver.quit();
}
@Test
public void home() {
HomePage home = HomePage.go(this.driver, this.port);
home.assertAt();
}
@Test
public void noAttributes() {
HomePage home = HomePage.go(this.driver, this.port);
assertThat(home.attributes()).isEmpty();
}
@Test
public void createAttribute() {
HomePage home = HomePage.go(this.driver, this.port);
// @formatter:off
home = home.form()
.attributeName("a")
.attributeValue("b")
.submit(HomePage.class);
// @formatter:on
List<Attribute> attributes = home.attributes();
assertThat(attributes).hasSize(1);
Attribute row = attributes.get(0);
assertThat(row.getAttributeName()).isEqualTo("a");
assertThat(row.getAttributeValue()).isEqualTo("b");
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.pages;
import java.util.ArrayList;
import java.util.List;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eddú Meléndez
* @author Rob Winch
*/
public class HomePage {
private WebDriver driver;
@FindBy(css = "form")
WebElement form;
@FindBy(css = "table tbody tr")
List<WebElement> trs;
List<Attribute> attributes;
public HomePage(WebDriver driver) {
this.driver = driver;
this.attributes = new ArrayList<>();
}
private static void get(WebDriver driver, int port, String get) {
String baseUrl = "http://localhost:" + port;
driver.get(baseUrl + get);
}
public static HomePage go(WebDriver driver, int port) {
get(driver, port, "/");
return PageFactory.initElements(driver, HomePage.class);
}
public void assertAt() {
assertThat(this.driver.getTitle()).isEqualTo("Session Attributes");
}
public List<Attribute> attributes() {
List<Attribute> rows = new ArrayList<>();
for (WebElement tr : this.trs) {
rows.add(new Attribute(tr));
}
this.attributes.addAll(rows);
return this.attributes;
}
public Form form() {
return new Form(this.form);
}
public class Form {
@FindBy(name = "attributeName")
WebElement attributeName;
@FindBy(name = "attributeValue")
WebElement attributeValue;
@FindBy(css = "input[type=\"submit\"]")
WebElement submit;
public Form(SearchContext context) {
PageFactory.initElements(new DefaultElementLocatorFactory(context), this);
}
public Form attributeName(String text) {
this.attributeName.sendKeys(text);
return this;
}
public Form attributeValue(String text) {
this.attributeValue.sendKeys(text);
return this;
}
public <T> T submit(Class<T> page) {
this.submit.click();
return PageFactory.initElements(HomePage.this.driver, page);
}
}
public static class Attribute {
@FindBy(xpath = ".//td[1]")
WebElement attributeName;
@FindBy(xpath = ".//td[2]")
WebElement attributeValue;
public Attribute(SearchContext context) {
PageFactory.initElements(new DefaultElementLocatorFactory(context), this);
}
/**
* @return the attributeName
*/
public String getAttributeName() {
return this.attributeName.getText();
}
/**
* @return the attributeValue
*/
public String getAttributeValue() {
return this.attributeValue.getText();
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import reactor.ipc.netty.NettyContext;
import reactor.ipc.netty.http.server.HttpServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* @author Rob Winch
* @since 5.0
*/
@Configuration
@EnableWebFlux
@ComponentScan
public class HelloWebfluxApplication {
@Value("${server.port:8080}")
private int port = 8080;
public static void main(String[] args) throws Exception {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
HelloWebfluxApplication.class)) {
context.getBean(NettyContext.class).onClose().block();
}
}
@Profile("default")
@Bean
public NettyContext nettyContext(ApplicationContext context) {
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer httpServer = HttpServer.create("localhost", this.port);
return httpServer.newHandler(adapter).block();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 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.
@@ -14,25 +14,24 @@
* limitations under the License.
*/
package org.springframework.session.data.redis.flushimmediately;
package sample;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.EnableSpringWebSession;
import org.springframework.session.MapReactorSessionRepository;
/**
* @author Rob Winch
*
*/
// tag::class[]
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
public class RedisHttpSessionConfig {
@EnableSpringWebSession
public class HelloWebfluxSessionConfig {
@Bean
public JedisConnectionFactory connectionFactory() throws Exception {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setUsePool(false);
return factory;
public MapReactorSessionRepository reactorSessionRepository() {
return new MapReactorSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]

View File

@@ -0,0 +1,91 @@
/*
* =============================================================================
*
* Copyright (c) 2011-2014, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* =============================================================================
*/
package sample;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SSEFluxWebConfig {
// TODO * Once there is a Spring Boot starter for thymeleaf-spring5, there would be no
// need to have
// TODO that @EnableConfigurationProperties annotation or use it for declaring the
// beans down in the
// TODO "thymeleaf" section below.
private ApplicationContext applicationContext;
public SSEFluxWebConfig(final ApplicationContext applicationContext) {
super();
this.applicationContext = applicationContext;
}
/*
* -------------------------------------- THYMELEAF CONFIGURATION
* --------------------------------------
*/
// TODO * If there was a Spring Boot starter for thymeleaf-spring5 most probably some
// or all of these
// TODO resolver and engine beans would not need to be specifically declared here.
@Bean
public SpringResourceTemplateResolver thymeleafTemplateResolver() {
final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(this.applicationContext);
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCacheable(false);
resolver.setCheckExistence(true);
return resolver;
}
@Bean
public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() {
// We override here the SpringTemplateEngine instance that would otherwise be
// instantiated by
// Spring Boot because we want to apply the SpringWebFlux-specific context
// factory, link builder...
final SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine();
templateEngine.setTemplateResolver(thymeleafTemplateResolver());
return templateEngine;
}
@Bean
public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() {
final ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
viewResolver.setTemplateEngine(thymeleafTemplateEngine());
viewResolver.setOrder(1);
viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit
return viewResolver;
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
/**
* @author Rob Winch
* @since 5.0
*/
public class SessionAttributeForm {
private String attributeName;
private String attributeValue;
public String getAttributeName() {
return this.attributeName;
}
public void setAttributeName(String attributeName) {
this.attributeName = attributeName;
}
public String getAttributeValue() {
return this.attributeValue;
}
public void setAttributeValue(String attributeValue) {
this.attributeValue = attributeValue;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014-2016 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.server.WebSession;
// tag::class[]
@Controller
public class SessionController {
@PostMapping("/session")
public String setAttribute(@ModelAttribute SessionAttributeForm sessionAttributeForm, WebSession session) {
session.getAttributes().put(sessionAttributeForm.getAttributeName(), sessionAttributeForm.getAttributeValue());
return "redirect:/";
}
@GetMapping("/")
public String index(Model model, WebSession webSession) {
model.addAttribute("webSession", webSession);
return "index";
}
private static final long serialVersionUID = 2878267318695777395L;
}
// tag::end[]

View File

@@ -0,0 +1,44 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
/**
* @author Rob Winch
* @since 5.0
*/
@Configuration
public class ThymeleafWebfluxConfig implements WebFluxConfigurer {
@Autowired(required = false)
List<ViewResolver> views = new ArrayList<>();
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
for (ViewResolver view : this.views) {
registry.viewResolver(view);
}
}
}

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- <logger name="org.springframework.security" level="DEBUG"/> -->
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>Session Attributes</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
<h1>Description</h1>
<p>This application demonstrates how to use a Redis instance to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.</p>
<h1>Try it</h1>
<form class="form-inline" role="form" action="./session" method="post">
<label for="attributeName">Attribute Name</label>
<input id="attributeName" type="text" name="attributeName"/>
<label for="attributeValue">Attribute Value</label>
<input id="attributeValue" type="text" name="attributeValue"/>
<input type="submit" value="Set Attribute"/>
</form>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>Attribute Name</th>
<th>Attribute Value</th>
</tr>
</thead>
<tbody>
<tr th:each="attr : ${webSession.attributes}">
<td th:text="${attr.key}"/></td>
<td th:text="${attr.value}"/></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -1,7 +1,7 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile project(':spring-session')
compile project(':spring-session-core')
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-taglib"
compile "com.hazelcast:hazelcast-client"

View File

@@ -23,52 +23,35 @@ import java.util.Map;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
@WebListener
public class Initializer implements ServletContextListener {
private static final String SESSION_MAP_NAME = "spring:session:sessions";
private HazelcastInstance instance;
public void contextInitialized(ServletContextEvent sce) {
String sessionMapName = "spring:session:sessions";
ServletContext sc = sce.getServletContext();
this.instance = createHazelcastInstance();
Map<String, Session> sessions = this.instance.getMap(SESSION_MAP_NAME);
Config cfg = new Config();
NetworkConfig netConfig = new NetworkConfig();
netConfig.setPort(getAvailablePort());
cfg.setNetworkConfig(netConfig);
SerializerConfig serializer = new SerializerConfig().setTypeClass(Object.class)
.setImplementation(new ObjectStreamSerializer());
cfg.getSerializationConfig().addSerializerConfig(serializer);
MapConfig mc = new MapConfig();
mc.setName(sessionMapName);
mc.setTimeToLiveSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
cfg.addMapConfig(mc);
this.instance = Hazelcast.newHazelcastInstance(cfg);
Map<String, ExpiringSession> sessions = this.instance.getMap(sessionMapName);
SessionRepository<ExpiringSession> sessionRepository = new MapSessionRepository(
sessions);
SessionRepositoryFilter<ExpiringSession> filter = new SessionRepositoryFilter<>(
MapSessionRepository sessionRepository = new MapSessionRepository(sessions);
SessionRepositoryFilter<? extends Session> filter = new SessionRepositoryFilter<>(
sessionRepository);
Dynamic fr = sc.addFilter("springSessionFilter", filter);
Dynamic fr = sce.getServletContext().addFilter("springSessionFilter", filter);
fr.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
@@ -76,6 +59,17 @@ public class Initializer implements ServletContextListener {
this.instance.shutdown();
}
private HazelcastInstance createHazelcastInstance() {
Config config = new Config();
config.getNetworkConfig().setPort(getAvailablePort());
config.getMapConfig(SESSION_MAP_NAME)
.setTimeToLiveSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
return Hazelcast.newHazelcastInstance(config);
}
private static int getAvailablePort() {
ServerSocket socket = null;
try {
@@ -93,4 +87,5 @@ public class Initializer implements ServletContextListener {
}
}
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2014-2016 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.StreamSerializer;
/**
* A {@link StreamSerializer} that uses Java serialization to persist the session. This is
* certainly not the most efficient way to persist sessions, but the example is intended
* to demonstrate using minimal dependencies. For better serialization methods try using
* <a href="https://github.com/EsotericSoftware/kryo">Kryo</a>.
*
* @author Rob Winch
*
*/
public class ObjectStreamSerializer implements StreamSerializer<Object> {
public int getTypeId() {
return 2;
}
public void write(ObjectDataOutput objectDataOutput, Object object)
throws IOException {
ObjectOutputStream out = new ObjectOutputStream((OutputStream) objectDataOutput);
out.writeObject(object);
out.flush();
}
public Object read(ObjectDataInput objectDataInput) throws IOException {
ObjectInputStream in = new ObjectInputStream((InputStream) objectDataInput);
try {
return in.readObject();
}
catch (ClassNotFoundException e) {
throw new IOException(e);
}
}
public void destroy() {
}
}

View File

@@ -1,15 +1,14 @@
apply plugin: 'io.spring.convention.spring-sample-war'
dependencies {
compile(project(':spring-session-data-redis')) {
exclude module: 'jedis'
}
compile project(':spring-session-data-redis')
compile "org.springframework:spring-web"
compile "io.lettuce:lettuce-core"
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-taglib"
compile jstlDependencies
compile slf4jDependencies
compile "org.testcontainers:testcontainers"
providedCompile "javax.servlet:javax.servlet-api"
@@ -18,3 +17,7 @@ dependencies {
integrationTestCompile seleniumDependencies
}
gretty {
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
}

View File

@@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.IOException;
import org.testcontainers.containers.GenericContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@Profile("embedded-redis")
public class EmbeddedRedisConfig {
private static final String REDIS_DOCKER_IMAGE = "redis:3.2.9";
@Bean(initMethod = "start")
public GenericContainer redisContainer() {
return new GenericContainer(REDIS_DOCKER_IMAGE) {
@Override
public void close() {
super.close();
try {
this.dockerClient.close();
}
catch (IOException ignored) {
}
}
}.withExposedPorts(6379);
}
@Bean
@Primary
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisContainer().getContainerIpAddress(),
redisContainer().getFirstMappedPort());
}
}

View File

@@ -15,4 +15,7 @@
<!--2-->
<bean class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory"/>
<!-- end::beans[] -->
</beans>
<bean class="sample.EmbeddedRedisConfig"/>
</beans>

View File

@@ -5,25 +5,18 @@ description = "Spring Session"
dependencies {
compile "org.springframework:spring-jcl"
optional "io.projectreactor:reactor-core"
optional "javax.servlet:javax.servlet-api"
optional "org.springframework:spring-context"
optional "org.springframework:spring-jdbc"
optional "org.springframework:spring-messaging"
optional "org.springframework:spring-web"
optional "org.springframework:spring-webflux"
optional "org.springframework:spring-websocket"
optional "org.springframework.data:spring-data-redis"
optional "org.springframework.security:spring-security-core"
optional "org.springframework.security:spring-security-web"
optional "com.hazelcast:hazelcast"
provided "javax.servlet:javax.servlet-api"
integrationTestCompile "redis.clients:jedis"
integrationTestCompile "org.apache.commons:commons-pool2"
integrationTestCompile "org.apache.derby:derby"
integrationTestCompile "com.h2database:h2"
integrationTestCompile "com.hazelcast:hazelcast-client"
integrationTestCompile "org.hsqldb:hsqldb"
testCompile "io.projectreactor:reactor-test"
testCompile "junit:junit"
testCompile "org.mockito:mockito-core"
testCompile "edu.umd.cs.mtc:multithreadedtc"

View File

@@ -0,0 +1,54 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://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;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* Add this annotation to a {@code @Configuration} class to configure a {@code WebSessionManager} for a WebFlux
* application. This annotation assumes a {@code ReactorSessionRepository} is defined somewhere in the application
* context. If not, it will fail with a clear error messages. For example:
*
* <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableSpringWebSession}
* public class SpringWebFluxConfig {
*
* {@literal @Bean}
* public ReactorSessionRepository sessionRepository() {
* return new MapReactorSessionRepository();
* }
*
* }
* </code>
* </pre>
*
* @author Greg Turnquist
* @since 2.0
*/
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringWebSessionConfiguration.class)
@Configuration
public @interface EnableSpringWebSession {
}

View File

@@ -0,0 +1,108 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://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;
import java.time.Duration;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
/**
* A {@link ReactorSessionRepository} backed by a {@link Map} and that uses a
* {@link MapSession}. The injected {@link java.util.Map} can be backed by a distributed
* NoSQL store like Hazelcast, for instance. Note that the supplied map itself is
* responsible for purging the expired sessions.
*
* <p>
* The implementation does NOT support firing {@link SessionDeletedEvent} or
* {@link SessionExpiredEvent}.
* </p>
*
* @author Rob Winch
* @since 2.0
*/
public class MapReactorSessionRepository implements ReactorSessionRepository<MapSession> {
/**
* If non-null, this value is used to override
* {@link Session#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private final Map<String, Session> sessions;
/**
* Creates a new instance backed by the provided {@link Map}. This allows injecting a
* distributed {@link Map}.
*
* @param sessions the {@link Map} to use. Cannot be null.
*/
public MapReactorSessionRepository(Map<String, Session> sessions) {
if (sessions == null) {
throw new IllegalArgumentException("sessions cannot be null");
}
this.sessions = sessions;
}
/**
* If non-null, this value is used to override
* {@link Session#setMaxInactiveInterval(Duration)}.
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = Integer.valueOf(defaultMaxInactiveInterval);
}
public Mono<Void> save(MapSession session) {
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));
});
}
public Mono<MapSession> findById(String id) {
// @formatter:off
return Mono.defer(() -> Mono.justOrEmpty(this.sessions.get(id))
.filter(session -> !session.isExpired())
.map(MapSession::new)
.switchIfEmpty(deleteById(id).then(Mono.empty())));
// @formatter:on
}
public Mono<Void> deleteById(String id) {
return Mono.fromRunnable(() -> this.sessions.remove(id));
}
public Mono<MapSession> createSession() {
return Mono.defer(() -> {
MapSession result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return Mono.just(result);
});
}
}

View File

@@ -17,11 +17,12 @@
package org.springframework.session;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* <p>
@@ -41,31 +42,34 @@ import java.util.concurrent.TimeUnit;
* </p>
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
*/
public final class MapSession implements ExpiringSession, Serializable {
public final class MapSession implements Session, Serializable {
/**
* Default {@link #setMaxInactiveIntervalInSeconds(int)} (30 minutes).
* Default {@link #setMaxInactiveInterval(Duration)} (30 minutes).
*/
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
private String id;
private String originalId;
private Map<String, Object> sessionAttrs = new HashMap<>();
private long creationTime = System.currentTimeMillis();
private long lastAccessedTime = this.creationTime;
private Instant creationTime = Instant.now();
private Instant lastAccessedTime = this.creationTime;
/**
* Defaults to 30 minutes.
*/
private int maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private Duration maxInactiveInterval = Duration.ofSeconds(DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
/**
* Creates a new instance with a secure randomly generated identifier.
*/
public MapSession() {
this(UUID.randomUUID().toString());
this(generateId());
}
/**
* Creates a new instance with the specified id. This is preferred to the default
* constructor when the id is known to prevent unnecessary consumption on entropy
@@ -75,6 +79,7 @@ public final class MapSession implements ExpiringSession, Serializable {
*/
public MapSession(String id) {
this.id = id;
this.originalId = id;
}
/**
@@ -83,27 +88,30 @@ public final class MapSession implements ExpiringSession, Serializable {
* @param session the {@link Session} to initialize this {@link Session} with. Cannot
* be null.
*/
public MapSession(ExpiringSession session) {
public MapSession(Session session) {
if (session == null) {
throw new IllegalArgumentException("session cannot be null");
}
this.id = session.getId();
this.originalId = this.id;
this.sessionAttrs = new HashMap<>(
session.getAttributeNames().size());
for (String attrName : session.getAttributeNames()) {
Object attrValue = session.getAttribute(attrName);
this.sessionAttrs.put(attrName, attrValue);
if (attrValue != null) {
this.sessionAttrs.put(attrName, attrValue);
}
}
this.lastAccessedTime = session.getLastAccessedTime();
this.creationTime = session.getCreationTime();
this.maxInactiveInterval = session.getMaxInactiveIntervalInSeconds();
this.maxInactiveInterval = session.getMaxInactiveInterval();
}
public void setLastAccessedTime(long lastAccessedTime) {
public void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}
public long getCreationTime() {
public Instant getCreationTime() {
return this.creationTime;
}
@@ -111,28 +119,41 @@ public final class MapSession implements ExpiringSession, Serializable {
return this.id;
}
public long getLastAccessedTime() {
String getOriginalId() {
return this.originalId;
}
void setOriginalId(String originalId) {
this.originalId = originalId;
}
public String changeSessionId() {
String changedId = generateId();
setId(changedId);
return changedId;
}
public Instant getLastAccessedTime() {
return this.lastAccessedTime;
}
public void setMaxInactiveIntervalInSeconds(int interval) {
public void setMaxInactiveInterval(Duration interval) {
this.maxInactiveInterval = interval;
}
public int getMaxInactiveIntervalInSeconds() {
public Duration getMaxInactiveInterval() {
return this.maxInactiveInterval;
}
public boolean isExpired() {
return isExpired(System.currentTimeMillis());
return isExpired(Instant.now());
}
boolean isExpired(long now) {
if (this.maxInactiveInterval < 0) {
boolean isExpired(Instant now) {
if (this.maxInactiveInterval.isNegative()) {
return false;
}
return now - TimeUnit.SECONDS
.toMillis(this.maxInactiveInterval) >= this.lastAccessedTime;
return now.minus(this.maxInactiveInterval).compareTo(this.lastAccessedTime) >= 0;
}
@SuppressWarnings("unchecked")
@@ -158,12 +179,11 @@ public final class MapSession implements ExpiringSession, Serializable {
}
/**
* Sets the time that this {@link Session} was created in milliseconds since midnight
* of 1/1/1970 GMT. The default is when the {@link Session} was instantiated.
* @param creationTime the time that this {@link Session} was created in milliseconds
* since midnight of 1/1/1970 GMT.
* Sets the time that this {@link Session} was created. The default is when the
* {@link Session} was instantiated.
* @param creationTime the time that this {@link Session} was created.
*/
public void setCreationTime(long creationTime) {
public void setCreationTime(Instant creationTime) {
this.creationTime = creationTime;
}
@@ -186,5 +206,9 @@ public final class MapSession implements ExpiringSession, Serializable {
return this.id.hashCode();
}
private static String generateId() {
return UUID.randomUUID().toString();
}
private static final long serialVersionUID = 7160779239673823561L;
}

View File

@@ -16,17 +16,17 @@
package org.springframework.session;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
/**
* A {@link SessionRepository} backed by a {@link java.util.Map} and that uses a
* {@link MapSession}. By default a {@link java.util.concurrent.ConcurrentHashMap} is
* used, but a custom {@link java.util.Map} can be injected to use distributed maps
* provided by NoSQL stores like Redis and Hazelcast.
* {@link MapSession}. The injected {@link java.util.Map} can be backed by a distributed
* NoSQL store like Hazelcast, for instance. Note that the supplied map itself is
* responsible for purging the expired sessions.
*
* <p>
* The implementation does NOT support firing {@link SessionDeletedEvent} or
@@ -36,21 +36,15 @@ import org.springframework.session.events.SessionExpiredEvent;
* @author Rob Winch
* @since 1.0
*/
public class MapSessionRepository implements SessionRepository<ExpiringSession> {
public class MapSessionRepository implements SessionRepository<MapSession> {
/**
* If non-null, this value is used to override
* {@link ExpiringSession#setMaxInactiveIntervalInSeconds(int)}.
* {@link Session#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private final Map<String, ExpiringSession> sessions;
/**
* Creates an instance backed by a {@link java.util.concurrent.ConcurrentHashMap}.
*/
public MapSessionRepository() {
this(new ConcurrentHashMap<>());
}
private final Map<String, Session> sessions;
/**
* Creates a new instance backed by the provided {@link java.util.Map}. This allows
@@ -58,7 +52,7 @@ public class MapSessionRepository implements SessionRepository<ExpiringSession>
*
* @param sessions the {@link java.util.Map} to use. Cannot be null.
*/
public MapSessionRepository(Map<String, ExpiringSession> sessions) {
public MapSessionRepository(Map<String, Session> sessions) {
if (sessions == null) {
throw new IllegalArgumentException("sessions cannot be null");
}
@@ -67,7 +61,7 @@ public class MapSessionRepository implements SessionRepository<ExpiringSession>
/**
* If non-null, this value is used to override
* {@link ExpiringSession#setMaxInactiveIntervalInSeconds(int)}.
* {@link Session#setMaxInactiveInterval(Duration)}.
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
@@ -75,31 +69,37 @@ public class MapSessionRepository implements SessionRepository<ExpiringSession>
this.defaultMaxInactiveInterval = Integer.valueOf(defaultMaxInactiveInterval);
}
public void save(ExpiringSession session) {
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));
}
public ExpiringSession getSession(String id) {
ExpiringSession saved = this.sessions.get(id);
public MapSession findById(String id) {
Session saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
delete(saved.getId());
deleteById(saved.getId());
return null;
}
return new MapSession(saved);
}
public void delete(String id) {
public void deleteById(String id) {
this.sessions.remove(id);
}
public ExpiringSession createSession() {
ExpiringSession result = new MapSession();
public MapSession createSession() {
MapSession result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://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;
import reactor.core.publisher.Mono;
/**
* A repository interface for managing {@link Session} instances.
*
* @param <S> the {@link Session} type
* @author Rob Winch
* @since 2.0
*/
public interface ReactorSessionRepository<S extends Session> {
/**
* Creates a new {@link Session} that is capable of being persisted by this
* {@link ReactorSessionRepository}.
*
* <p>
* This allows optimizations and customizations in how the {@link Session} is
* persisted. For example, the implementation returned might keep track of the changes
* ensuring that only the delta needs to be persisted on a save.
* </p>
*
* @return a new {@link Session} that is capable of being persisted by this
* {@link ReactorSessionRepository}
*/
Mono<S> createSession();
/**
* Ensures the {@link Session} created by
* {@link ReactorSessionRepository#createSession()} is saved.
*
* <p>
* Some implementations may choose to save as the {@link Session} is updated by
* returning a {@link Session} that immediately persists any changes. In this case,
* this method may not actually do anything.
* </p>
*
* @param session the {@link Session} to save
* @return indicator of operation completion
*/
Mono<Void> save(S session);
/**
* Gets the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*
* @param id the {@link Session#getId()} to lookup
* @return the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*/
Mono<S> findById(String id);
/**
* Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
* if the {@link Session} is not found.
* @param id the {@link Session#getId()} to delete
* @return indicator of operation completion
*/
Mono<Void> deleteById(String id);
}

View File

@@ -0,0 +1,160 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://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;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
/**
* Provides a way to identify a user in an agnostic way. This allows the session to be
* used by an HttpSession, WebSocket Session, or even non web related sessions.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
*/
public interface Session {
/**
* Gets a unique string that identifies the {@link Session}.
*
* @return a unique string that identifies the {@link Session}
*/
String getId();
/**
* Changes the session id. After invoking the {@link #getId()} will return a new identifier.
* @return the new session id which {@link #getId()} will now return
*/
String changeSessionId();
/**
* Gets the Object associated with the specified name or null if no Object is
* associated to that name.
*
* @param <T> The return type of the attribute
* @param attributeName the name of the attribute to get
* @return the Object associated with the specified name or null if no Object is
* associated to that name
*/
<T> T getAttribute(String attributeName);
/**
* Return the session attribute value or if not present raise an
* {@link IllegalArgumentException}.
* @param name the attribute name
* @param <T> the attribute type
* @return the attribute value
*/
@SuppressWarnings("unchecked")
default <T> T getRequiredAttribute(String name) {
T result = getAttribute(name);
if (result == null) {
throw new IllegalArgumentException(
"Required attribute '" + name + "' is missing.");
}
return result;
}
/**
* Return the session attribute value, or a default, fallback value.
* @param name the attribute name
* @param defaultValue a default value to return instead
* @param <T> the attribute type
* @return the attribute value
*/
@SuppressWarnings("unchecked")
default <T> T getAttributeOrDefault(String name, T defaultValue) {
T result = getAttribute(name);
return result == null ? defaultValue : result;
}
/**
* Gets the attribute names that have a value associated with it. Each value can be
* passed into {@link org.springframework.session.Session#getAttribute(String)} to
* obtain the attribute value.
*
* @return the attribute names that have a value associated with it.
* @see #getAttribute(String)
*/
Set<String> getAttributeNames();
/**
* Sets the attribute value for the provided attribute name. If the attributeValue is
* null, it has the same result as removing the attribute with
* {@link org.springframework.session.Session#removeAttribute(String)} .
*
* @param attributeName the attribute name to set
* @param attributeValue the value of the attribute to set. If null, the attribute
* will be removed.
*/
void setAttribute(String attributeName, Object attributeValue);
/**
* Removes the attribute with the provided attribute name.
* @param attributeName the name of the attribute to remove
*/
void removeAttribute(String attributeName);
/**
* Gets the time when this session was created.
*
* @return the time when this session was created.
*/
Instant getCreationTime();
/**
* Sets the last accessed time.
*
* @param lastAccessedTime the last accessed time
*/
void setLastAccessedTime(Instant lastAccessedTime);
/**
* Gets the last time this {@link Session} was accessed.
*
* @return the last time the client sent a request associated with the session
*/
Instant getLastAccessedTime();
/**
* Sets the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*
* @param interval the amount of time that the {@link Session} should be kept alive
* between client requests.
*/
void setMaxInactiveInterval(Duration interval);
/**
* Gets the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*
* @return the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*/
Duration getMaxInactiveInterval();
/**
* Returns true if the session is expired.
*
* @return true if the session is expired, else false.
*/
boolean isExpired();
}

View File

@@ -62,12 +62,12 @@ public interface SessionRepository<S extends Session> {
* @return the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*/
S getSession(String id);
S findById(String id);
/**
* Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
* if the {@link Session} is not found.
* @param id the {@link org.springframework.session.Session#getId()} to delete
*/
void delete(String id);
void deleteById(String id);
}

View File

@@ -0,0 +1,63 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.server.session.SpringSessionWebSessionStore;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionIdResolver;
import org.springframework.web.server.session.WebSessionManager;
/**
* Wire up a {@link WebSessionManager} using a Reactive {@link ReactorSessionRepository} from the application context.
*
* @author Greg Turnquist
* @author Rob Winch
* @since 2.0
*
* @see EnableSpringWebSession
*/
@Configuration
public class SpringWebSessionConfiguration {
/**
* Optional override of default {@link WebSessionIdResolver}.
*/
@Autowired(required = false)
private WebSessionIdResolver webSessionIdResolver;
/**
* Configure a {@link WebSessionManager} using a provided {@link ReactorSessionRepository}.
*
* @param repository - a bean that implements {@link ReactorSessionRepository}.
* @return a configured {@link WebSessionManager} registered with a preconfigured name.
*/
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
public WebSessionManager webSessionManager(ReactorSessionRepository<? extends Session> repository) {
SpringSessionWebSessionStore<? extends Session> sessionStore = new SpringSessionWebSessionStore<>(repository);
DefaultWebSessionManager manager = new DefaultWebSessionManager();
manager.setSessionStore(sessionStore);
if (this.webSessionIdResolver != null) {
manager.setSessionIdResolver(this.webSessionIdResolver);
}
return manager;
}
}

View File

@@ -33,7 +33,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
@@ -122,7 +122,7 @@ public class SpringHttpSessionConfiguration implements ApplicationContextAware {
}
@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);

View File

@@ -23,7 +23,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
@@ -32,12 +31,12 @@ import org.springframework.session.SessionRepository;
* Ensures that calling {@link #expireNow()} propagates to Spring Session, since this
* session information contains only derived data and is not the authoritative source.
*
* @param <S> the {@link ExpiringSession} type.
* @param <S> the {@link Session} type.
* @author Joris Kuipers
* @author Vedran Pavic
* @since 1.3
*/
class SpringSessionBackedSessionInformation<S extends ExpiringSession>
class SpringSessionBackedSessionInformation<S extends Session>
extends SessionInformation {
static final String EXPIRED_ATTR = SpringSessionBackedSessionInformation.class
@@ -53,9 +52,10 @@ class SpringSessionBackedSessionInformation<S extends ExpiringSession>
SpringSessionBackedSessionInformation(S session,
SessionRepository<S> sessionRepository) {
super(resolvePrincipal(session), session.getId(),
new Date(session.getLastAccessedTime()));
Date.from(session.getLastAccessedTime()));
this.sessionRepository = sessionRepository;
if (Boolean.TRUE.equals(session.getAttribute(EXPIRED_ATTR))) {
Boolean expired = session.getAttribute(EXPIRED_ATTR);
if (Boolean.TRUE.equals(expired)) {
super.expireNow();
}
}
@@ -72,8 +72,10 @@ class SpringSessionBackedSessionInformation<S extends ExpiringSession>
if (principalName != null) {
return principalName;
}
SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (securityContext != null && securityContext.getAuthentication() != null) {
SecurityContext securityContext = session
.getAttribute(SPRING_SECURITY_CONTEXT);
if (securityContext != null
&& securityContext.getAuthentication() != null) {
return securityContext.getAuthentication().getName();
}
return "";
@@ -87,7 +89,7 @@ class SpringSessionBackedSessionInformation<S extends ExpiringSession>
+ "sessions was exceeded");
}
super.expireNow();
S session = this.sessionRepository.getSession(getSessionId());
S session = this.sessionRepository.findById(getSessionId());
if (session != null) {
session.setAttribute(EXPIRED_ATTR, Boolean.TRUE);
this.sessionRepository.save(session);

View File

@@ -24,8 +24,8 @@ import java.util.List;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
@@ -39,12 +39,12 @@ import org.springframework.util.Assert;
* <p>
* Does not support {@link #getAllPrincipals()}, since that information is not available.
*
* @param <S> the {@link ExpiringSession} type.
* @param <S> the {@link Session} type.
* @author Joris Kuipers
* @author Vedran Pavic
* @since 1.3
*/
public class SpringSessionBackedSessionRegistry<S extends ExpiringSession>
public class SpringSessionBackedSessionRegistry<S extends Session>
implements SessionRegistry {
private final FindByIndexNameSessionRepository<S> sessionRepository;
@@ -70,7 +70,7 @@ public class SpringSessionBackedSessionRegistry<S extends ExpiringSession>
for (S session : sessions) {
if (includeExpiredSessions || !Boolean.TRUE.equals(session
.getAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR))) {
infos.add(new SpringSessionBackedSessionInformation<S>(session,
infos.add(new SpringSessionBackedSessionInformation<>(session,
this.sessionRepository));
}
}
@@ -78,9 +78,9 @@ public class SpringSessionBackedSessionRegistry<S extends ExpiringSession>
}
public SessionInformation getSessionInformation(String sessionId) {
S session = this.sessionRepository.getSession(sessionId);
S session = this.sessionRepository.findById(sessionId);
if (session != null) {
return new SpringSessionBackedSessionInformation<S>(session,
return new SpringSessionBackedSessionInformation<>(session,
this.sessionRepository);
}
return null;

Some files were not shown because too many files have changed in this diff Show More