Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33fbaa03a8 | ||
|
|
88b26f2cfe | ||
|
|
3f670050ef | ||
|
|
e3b61d25bb | ||
|
|
19b8effa41 | ||
|
|
9f5f7540d2 | ||
|
|
eb8c22939c | ||
|
|
45cfa1e9a4 | ||
|
|
99221e0948 | ||
|
|
41cf2ef152 | ||
|
|
c51bce4777 | ||
|
|
b6f1184c4c | ||
|
|
c69a8b8762 | ||
|
|
99fb17a66b | ||
|
|
937b2fcbf1 | ||
|
|
9c5a7e9156 | ||
|
|
4deccd3ad0 | ||
|
|
da058e9510 | ||
|
|
d28ca4658b | ||
|
|
c14fdb283d | ||
|
|
ee1ff3ed3b | ||
|
|
eb7bcc5eeb | ||
|
|
188e5ba4e0 | ||
|
|
1e46630467 | ||
|
|
b72c600884 | ||
|
|
274aec1691 | ||
|
|
52ea98b4ce | ||
|
|
5c294ae1d2 | ||
|
|
1752928d96 | ||
|
|
0cdee25405 | ||
|
|
4a9f1700d5 | ||
|
|
36ab358d24 | ||
|
|
8e3371aed9 | ||
|
|
2161f966de | ||
|
|
63b67a501d | ||
|
|
2b0431eae4 | ||
|
|
04ec086014 | ||
|
|
5697f49a71 | ||
|
|
dfce66383f | ||
|
|
a83e59bf52 | ||
|
|
8b233e84ef | ||
|
|
84e7fbace1 | ||
|
|
f455df3333 | ||
|
|
a7bb9d3b31 | ||
|
|
5f0e4c3b85 | ||
|
|
23c6c7cf31 | ||
|
|
c8c5fae678 | ||
|
|
f4a58622e4 | ||
|
|
5384764021 |
12
.travis.yml
12
.travis.yml
@@ -1,20 +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/
|
||||
|
||||
install: true
|
||||
|
||||
script: ./gradlew clean build --refresh-dependencies --no-daemon
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.3.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.4.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
repositories {
|
||||
|
||||
@@ -109,7 +109,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -92,7 +92,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -84,7 +84,8 @@ server.session.timeout=60
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -85,7 +85,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
@@ -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
|
||||
|
||||
@@ -79,7 +79,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -126,7 +126,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -127,7 +127,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -131,7 +131,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -20,7 +20,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -134,7 +134,8 @@ You can run the sample by obtaining the {download-url}[source code] and invoking
|
||||
[NOTE]
|
||||
====
|
||||
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.
|
||||
Alternatively, you can update the `RedisConnectionFactory` to point to a Redis server.
|
||||
Another option is to use https://www.docker.com/[Docker] to run Redis on localhost. See https://hub.docker.com/_/redis/[Docker Redis repository] for detailed instructions.
|
||||
====
|
||||
|
||||
----
|
||||
|
||||
@@ -666,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.
|
||||
@@ -683,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]
|
||||
====
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package docs;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
@@ -51,7 +52,7 @@ public class IndexDocTests {
|
||||
@Test
|
||||
public void repositoryDemo() {
|
||||
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
|
||||
demo.repository = new MapSessionRepository();
|
||||
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
|
||||
demo.demo();
|
||||
}
|
||||
@@ -83,7 +84,7 @@ public class IndexDocTests {
|
||||
@Test
|
||||
public void expireRepositoryDemo() {
|
||||
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
|
||||
demo.repository = new MapSessionRepository();
|
||||
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
|
||||
demo.demo();
|
||||
}
|
||||
@@ -121,7 +122,8 @@ public class IndexDocTests {
|
||||
@SuppressWarnings("unused")
|
||||
public void mapRepository() {
|
||||
// tag::new-mapsessionrepository[]
|
||||
SessionRepository<? extends Session> repository = new MapSessionRepository();
|
||||
SessionRepository<? extends Session> repository = new MapSessionRepository(
|
||||
new ConcurrentHashMap<>());
|
||||
// end::new-mapsessionrepository[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
springBootVersion=2.0.0.M2
|
||||
version=2.0.0.M3
|
||||
springBootVersion=2.0.0.M4
|
||||
version=2.0.0.M5
|
||||
|
||||
@@ -1,37 +1,55 @@
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'io.projectreactor:reactor-bom:Bismuth-M3'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.0.0.RC3'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-RC1'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.0.0.M3'
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.1'
|
||||
mavenBom 'io.projectreactor:reactor-bom:Bismuth-RELEASE'
|
||||
mavenBom 'org.springframework:spring-framework-bom:5.0.0.RELEASE'
|
||||
mavenBom 'org.springframework.data:spring-data-releasetrain:Kay-RELEASE'
|
||||
mavenBom 'org.springframework.security:spring-security-bom:5.0.0.M5'
|
||||
}
|
||||
|
||||
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.6') {
|
||||
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.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.RC1'
|
||||
dependency 'io.lettuce:lettuce-core:5.0.0.RELEASE'
|
||||
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.44'
|
||||
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.1.2'
|
||||
dependency 'org.mockito:mockito-core:2.10.0'
|
||||
dependency 'org.postgresql:postgresql:42.1.4'
|
||||
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.1.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'
|
||||
dependency 'org.webjars:jquery:1.12.4'
|
||||
dependency 'org.webjars:knockout:2.3.0'
|
||||
dependency 'org.webjars:sockjs-client:0.3.4'
|
||||
dependency 'org.webjars:stomp-websocket:2.3.0'
|
||||
dependency 'org.webjars:webjars-taglib:0.3'
|
||||
dependency 'redis.clients:jedis:2.9.0'
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
||||
#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.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip
|
||||
|
||||
6
gradlew
vendored
6
gradlew
vendored
@@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS=""
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
@@ -155,7 +155,7 @@ if $cygwin ; then
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save ( ) {
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-data-redis"
|
||||
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 "org.springframework.session:spring-session"
|
||||
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
|
||||
compile "org.webjars:bootstrap"
|
||||
compile "org.webjars:html5shiv"
|
||||
@@ -18,12 +18,7 @@ dependencies {
|
||||
testCompile "org.assertj:assertj-core"
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
doFirst {
|
||||
systemProperties['spring.session.redis.namespace'] = project.name
|
||||
}
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,27 +16,46 @@
|
||||
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
|
||||
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.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
/**
|
||||
* Spring Security configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(
|
||||
User.withUsername("user").password("password").roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
// tag::config[]
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated();
|
||||
.permitAll();
|
||||
}
|
||||
// end::config[]
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -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:/";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
spring.session.store-type=redis
|
||||
security.user.password=password
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
dependencyManagement {
|
||||
dependencies {
|
||||
dependency 'io.lettuce:lettuce-core:5.0.0.M2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-jdbc"
|
||||
compile project(':spring-session-jdbc')
|
||||
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-devtools"
|
||||
compile "org.springframework.session:spring-session"
|
||||
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
|
||||
compile "org.webjars:bootstrap"
|
||||
compile "org.webjars:html5shiv"
|
||||
|
||||
@@ -16,17 +16,33 @@
|
||||
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
|
||||
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.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
/**
|
||||
* Spring Security configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(
|
||||
User.withUsername("user").password("password").roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void configure(WebSecurity web) throws Exception {
|
||||
@@ -35,4 +51,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
// tag::config[]
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.permitAll();
|
||||
}
|
||||
// end::config[]
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
spring.session.store-type=jdbc
|
||||
security.user.password=password
|
||||
spring.h2.console.enabled=true
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-data-redis"
|
||||
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 "org.springframework.session:spring-session"
|
||||
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"
|
||||
@@ -19,4 +18,5 @@ dependencies {
|
||||
testCompile "org.skyscreamer:jsonassert"
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,28 +13,46 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
|
||||
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.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
/**
|
||||
* @author jitendra on 3/3/16.
|
||||
* Spring Security configuration.
|
||||
*
|
||||
* @author jitendra
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(
|
||||
User.withUsername("user").password("password").roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated();
|
||||
.permitAll();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
spring.session.store-type=redis
|
||||
security.user.password=password
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-data-redis"
|
||||
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 "org.springframework.session:spring-session"
|
||||
compile "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
|
||||
compile "org.webjars:bootstrap"
|
||||
compile "org.webjars:html5shiv"
|
||||
@@ -15,4 +15,5 @@ dependencies {
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,12 +16,45 @@
|
||||
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
|
||||
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.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
/**
|
||||
* Spring Security configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(
|
||||
User.withUsername("user").password("password").roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
// tag::config[]
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.permitAll();
|
||||
}
|
||||
// end::config[]
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
spring.session.store-type=redis
|
||||
security.user.password=password
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-data-redis"
|
||||
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.session:spring-session"
|
||||
compile "org.springframework:spring-websocket"
|
||||
compile "org.springframework.security:spring-security-messaging"
|
||||
compile "org.springframework.security:spring-security-data"
|
||||
@@ -19,9 +19,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"
|
||||
}
|
||||
|
||||
@@ -17,25 +17,30 @@
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
@Configuration
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void configure(WebSecurity web) throws Exception {
|
||||
web
|
||||
.ignoring().antMatchers("/h2-console/**");
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(
|
||||
User.withUsername("user").password("password").roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
@Autowired
|
||||
@@ -47,4 +52,25 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void configure(WebSecurity web) throws Exception {
|
||||
web
|
||||
.ignoring().antMatchers("/h2-console/**");
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.permitAll();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
spring.session.store-type=redis
|
||||
#server.session.timeout=60
|
||||
spring.h2.console.enabled=true
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
compile "org.webjars:webjars-taglib"
|
||||
compile jstlDependencies
|
||||
compile slf4jDependencies
|
||||
compile "org.testcontainers:testcontainers"
|
||||
|
||||
providedCompile "javax.servlet:javax.servlet-api"
|
||||
|
||||
@@ -16,3 +17,7 @@ dependencies {
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
gretty {
|
||||
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
compile "org.webjars:webjars-taglib"
|
||||
compile jstlDependencies
|
||||
compile slf4jDependencies
|
||||
compile "org.testcontainers:testcontainers"
|
||||
|
||||
providedCompile "javax.servlet:javax.servlet-api"
|
||||
|
||||
@@ -16,3 +17,7 @@ dependencies {
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
gretty {
|
||||
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,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"
|
||||
|
||||
@@ -17,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']
|
||||
}
|
||||
|
||||
@@ -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.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 Session> sessionRepositoryFilter;
|
||||
private static final String DOCKER_IMAGE = "redis:4.0.2";
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -10,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"
|
||||
@@ -20,3 +21,7 @@ dependencies {
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
gretty {
|
||||
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencyManagement {
|
||||
dependencies {
|
||||
dependency 'org.webjars:bootstrap:3.3.6'
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
compile "org.webjars:webjars-taglib"
|
||||
compile jstlDependencies
|
||||
compile slf4jDependencies
|
||||
compile "org.testcontainers:testcontainers"
|
||||
|
||||
providedCompile "javax.servlet:javax.servlet-api"
|
||||
|
||||
@@ -17,3 +18,7 @@ dependencies {
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
gretty {
|
||||
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-session-data-redis')
|
||||
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
|
||||
compile 'org.testcontainers:testcontainers'
|
||||
|
||||
testCompile 'junit:junit'
|
||||
testCompile 'org.assertj:assertj-core'
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
@@ -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 = { "spring.profiles.active=embedded-redis", "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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.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();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -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,23 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.data.redis.flushimmediately;
|
||||
package sample;
|
||||
|
||||
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.context.annotation.Import;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.session.data.redis.config.annotation.web.reactor.EnableRedisReactorSession;
|
||||
|
||||
@Import(EmbeddedRedisConfig.class)
|
||||
// tag::class[]
|
||||
@EnableRedisReactorSession
|
||||
public class HelloWebfluxSessionConfig {
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
|
||||
public class RedisHttpSessionConfig {
|
||||
@Bean
|
||||
public JedisConnectionFactory connectionFactory() throws Exception {
|
||||
return new JedisConnectionFactory();
|
||||
public LettuceConnectionFactory lettuceConnectionFactory() {
|
||||
return new LettuceConnectionFactory();
|
||||
}
|
||||
}
|
||||
// end::class[]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
samples/javaconfig/webflux/src/main/resources/logback.xml
Normal file
14
samples/javaconfig/webflux/src/main/resources/logback.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
compile "org.webjars:webjars-taglib"
|
||||
compile jstlDependencies
|
||||
compile slf4jDependencies
|
||||
compile "org.testcontainers:testcontainers"
|
||||
|
||||
providedCompile "javax.servlet:javax.servlet-api"
|
||||
|
||||
@@ -16,3 +17,7 @@ dependencies {
|
||||
|
||||
integrationTestCompile seleniumDependencies
|
||||
}
|
||||
|
||||
gretty {
|
||||
jvmArgs = ['-Dspring.profiles.active=embedded-redis']
|
||||
}
|
||||
|
||||
@@ -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:4.0.2";
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,4 +15,7 @@
|
||||
<!--2-->
|
||||
<bean class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory"/>
|
||||
<!-- end::beans[] -->
|
||||
</beans>
|
||||
|
||||
<bean class="sample.EmbeddedRedisConfig"/>
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package org.springframework.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -26,10 +25,10 @@ import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionExpiredEvent;
|
||||
|
||||
/**
|
||||
* A {@link SessionRepository} backed by a {@link Map} and that uses a
|
||||
* {@link MapSession}. By default a {@link ConcurrentHashMap} is
|
||||
* used, but a custom {@link Map} can be injected to use distributed maps
|
||||
* provided by NoSQL stores like Redis and Hazelcast.
|
||||
* 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
|
||||
@@ -40,6 +39,7 @@ import org.springframework.session.events.SessionExpiredEvent;
|
||||
* @since 2.0
|
||||
*/
|
||||
public class MapReactorSessionRepository implements ReactorSessionRepository<MapSession> {
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override
|
||||
* {@link Session#setMaxInactiveInterval(Duration)}.
|
||||
@@ -49,15 +49,8 @@ public class MapReactorSessionRepository implements ReactorSessionRepository<Map
|
||||
private final Map<String, Session> sessions;
|
||||
|
||||
/**
|
||||
* Creates an instance backed by a {@link ConcurrentHashMap}.
|
||||
*/
|
||||
public MapReactorSessionRepository() {
|
||||
this(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance backed by the provided {@link Map}. This allows
|
||||
* injecting a distributed {@link Map}.
|
||||
* 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.
|
||||
*/
|
||||
@@ -68,38 +61,6 @@ public class MapReactorSessionRepository implements ReactorSessionRepository<Map
|
||||
this.sessions = 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(Session... sessions) {
|
||||
if (sessions == null) {
|
||||
throw new IllegalArgumentException("sessions cannot be null");
|
||||
}
|
||||
this.sessions = new ConcurrentHashMap<>();
|
||||
for (Session session : sessions) {
|
||||
this.performSave(new MapSession(session));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Iterable<Session> sessions) {
|
||||
if (sessions == null) {
|
||||
throw new IllegalArgumentException("sessions cannot be null");
|
||||
}
|
||||
this.sessions = new ConcurrentHashMap<>();
|
||||
for (Session session : sessions) {
|
||||
this.performSave(new MapSession(session));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override
|
||||
* {@link Session#setMaxInactiveInterval(Duration)}.
|
||||
@@ -111,32 +72,25 @@ public class MapReactorSessionRepository implements ReactorSessionRepository<Map
|
||||
}
|
||||
|
||||
public Mono<Void> save(MapSession session) {
|
||||
return Mono.fromRunnable(() -> performSave(session));
|
||||
}
|
||||
|
||||
private void performSave(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 Mono<MapSession> findById(String id) {
|
||||
return Mono.defer(() -> {
|
||||
Session saved = this.sessions.get(id);
|
||||
if (saved == null) {
|
||||
return Mono.empty();
|
||||
return Mono.fromRunnable(() -> {
|
||||
if (!session.getId().equals(session.getOriginalId())) {
|
||||
this.sessions.remove(session.getOriginalId());
|
||||
session.setOriginalId(session.getId());
|
||||
}
|
||||
if (saved.isExpired()) {
|
||||
delete(saved.getId());
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(new MapSession(saved));
|
||||
this.sessions.put(session.getId(), new MapSession(session));
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Void> delete(String id) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -150,4 +104,5 @@ public class MapReactorSessionRepository implements ReactorSessionRepository<Map
|
||||
return Mono.just(result);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,16 +18,15 @@ 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
|
||||
@@ -38,6 +37,7 @@ import org.springframework.session.events.SessionExpiredEvent;
|
||||
* @since 1.0
|
||||
*/
|
||||
public class MapSessionRepository implements SessionRepository<MapSession> {
|
||||
|
||||
/**
|
||||
* If non-null, this value is used to override
|
||||
* {@link Session#setMaxInactiveInterval(Duration)}.
|
||||
@@ -46,13 +46,6 @@ public class MapSessionRepository implements SessionRepository<MapSession> {
|
||||
|
||||
private final Map<String, Session> sessions;
|
||||
|
||||
/**
|
||||
* Creates an instance backed by a {@link java.util.concurrent.ConcurrentHashMap}.
|
||||
*/
|
||||
public MapSessionRepository() {
|
||||
this(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance backed by the provided {@link java.util.Map}. This allows
|
||||
* injecting a distributed {@link java.util.Map}.
|
||||
@@ -108,4 +101,5 @@ public class MapSessionRepository implements SessionRepository<MapSession> {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,5 +73,6 @@ public interface ReactorSessionRepository<S extends Session> {
|
||||
* @param id the {@link Session#getId()} to delete
|
||||
* @return indicator of operation completion
|
||||
*/
|
||||
Mono<Void> delete(String id);
|
||||
Mono<Void> deleteById(String id);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -55,13 +55,19 @@ public interface CookieSerializer {
|
||||
* {@link HttpServletResponse}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Vedran Pavic
|
||||
* @since 1.1
|
||||
*/
|
||||
class CookieValue {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final HttpServletResponse response;
|
||||
|
||||
private final String cookieValue;
|
||||
|
||||
private int cookieMaxAge = -1;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
@@ -72,11 +78,14 @@ public interface CookieSerializer {
|
||||
* modified by the {@link CookieSerializer} when writing to the actual cookie so
|
||||
* long as the original value is returned when the cookie is read.
|
||||
*/
|
||||
public CookieValue(HttpServletRequest request, HttpServletResponse response,
|
||||
CookieValue(HttpServletRequest request, HttpServletResponse response,
|
||||
String cookieValue) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.cookieValue = cookieValue;
|
||||
if ("".equals(this.cookieValue)) {
|
||||
this.cookieMaxAge = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,5 +114,24 @@ public interface CookieSerializer {
|
||||
public String getCookieValue() {
|
||||
return this.cookieValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cookie max age. The default is -1 which signals to delete the cookie
|
||||
* when the browser is closed, or 0 if cookie value is empty.
|
||||
* @return the cookie max age
|
||||
*/
|
||||
public int getCookieMaxAge() {
|
||||
return this.cookieMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cookie max age.
|
||||
* @param cookieMaxAge the cookie max age
|
||||
*/
|
||||
public void setCookieMaxAge(int cookieMaxAge) {
|
||||
this.cookieMaxAge = cookieMaxAge;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
|
||||
private String cookiePath;
|
||||
|
||||
private int cookieMaxAge = -1;
|
||||
private Integer cookieMaxAge;
|
||||
|
||||
private String domainName;
|
||||
|
||||
@@ -112,18 +112,18 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
sessionCookie.setHttpOnly(true);
|
||||
}
|
||||
|
||||
if ("".equals(requestedCookieValue)) {
|
||||
sessionCookie.setMaxAge(0);
|
||||
}
|
||||
else if (this.rememberMeRequestAttribute != null
|
||||
&& request.getAttribute(this.rememberMeRequestAttribute) != null) {
|
||||
// the cookie is only written at time of session creation, so we rely on
|
||||
// session expiration rather than cookie expiration if remember me is enabled
|
||||
sessionCookie.setMaxAge(Integer.MAX_VALUE);
|
||||
}
|
||||
else {
|
||||
sessionCookie.setMaxAge(this.cookieMaxAge);
|
||||
if (cookieValue.getCookieMaxAge() < 0) {
|
||||
if (this.rememberMeRequestAttribute != null
|
||||
&& request.getAttribute(this.rememberMeRequestAttribute) != null) {
|
||||
// the cookie is only written at time of session creation, so we rely on
|
||||
// session expiration rather than cookie expiration if remember me is enabled
|
||||
cookieValue.setCookieMaxAge(Integer.MAX_VALUE);
|
||||
}
|
||||
else if (this.cookieMaxAge != null) {
|
||||
cookieValue.setCookieMaxAge(this.cookieMaxAge);
|
||||
}
|
||||
}
|
||||
sessionCookie.setMaxAge(cookieValue.getCookieMaxAge());
|
||||
|
||||
response.addCookie(sessionCookie);
|
||||
}
|
||||
@@ -205,8 +205,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maxAge property of the Cookie. The default is -1 which signals to delete
|
||||
* the cookie when the browser is closed.
|
||||
* Sets the maxAge property of the Cookie. The default is to delete the cookie when
|
||||
* the browser is closed.
|
||||
*
|
||||
* @param cookieMaxAge the maxAge property of the Cookie
|
||||
*/
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* 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.web.server.session;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.session.ReactorSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.CookieWebSessionIdResolver;
|
||||
import org.springframework.web.server.session.WebSessionIdResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionStore;
|
||||
|
||||
/**
|
||||
* The {@link WebSessionManager} implementation backed by
|
||||
* {@link ReactorSessionRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class SpringSessionWebSessionManager implements WebSessionManager {
|
||||
|
||||
private final SpringSessionWebSessionStore<? extends Session> sessionStore;
|
||||
|
||||
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
|
||||
|
||||
private Clock clock = Clock.system(ZoneOffset.UTC);
|
||||
|
||||
public SpringSessionWebSessionManager(
|
||||
ReactorSessionRepository<? extends Session> sessionRepository) {
|
||||
this.sessionStore = new SpringSessionWebSessionStore<>(sessionRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link WebSessionIdResolver}.
|
||||
* @return the configured {@link WebSessionIdResolver}
|
||||
*/
|
||||
private WebSessionIdResolver getSessionIdResolver() {
|
||||
return this.sessionIdResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the id resolution strategy.
|
||||
* <p>
|
||||
* By default an instance of {@link CookieWebSessionIdResolver}.
|
||||
* @param sessionIdResolver the resolver to use
|
||||
*/
|
||||
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
|
||||
Assert.notNull(sessionIdResolver, "WebSessionIdResolver is required.");
|
||||
this.sessionIdResolver = sessionIdResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link WebSessionStore}.
|
||||
* @return the configured {@link WebSessionStore}
|
||||
*/
|
||||
private WebSessionStore getSessionStore() {
|
||||
return this.sessionStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured clock for session {@code lastAccessTime} calculations.
|
||||
* @return the configured clock for session {@code lastAccessTime} calculations
|
||||
*/
|
||||
private Clock getClock() {
|
||||
return this.clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link Clock} to use to set lastAccessTime on every created session
|
||||
* and to calculate if it is expired.
|
||||
* <p>
|
||||
* This may be useful to align to different timezone or to set the clock back in a
|
||||
* test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))} in order to
|
||||
* simulate session expiration.
|
||||
* <p>
|
||||
* By default this is {@code Clock.system(ZoneOffset.UTC)}.
|
||||
* @param clock the clock to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "'clock' is required.");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession(ServerWebExchange exchange) {
|
||||
// @formatter:off
|
||||
return Mono.defer(() ->
|
||||
retrieveSession(exchange)
|
||||
.flatMap(session -> removeSessionIfExpired(exchange, session))
|
||||
.flatMap(session -> {
|
||||
Instant lastAccessTime = Instant.now(getClock());
|
||||
return this.sessionStore.setLastAccessedTime(session, lastAccessTime);
|
||||
})
|
||||
.switchIfEmpty(createSession(exchange))
|
||||
.doOnNext(session -> exchange.getResponse().beforeCommit(session::save)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
|
||||
// @formatter:off
|
||||
return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange))
|
||||
.concatMap(this.sessionStore::retrieveSession)
|
||||
.cast(WebSession.class)
|
||||
.next();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Mono<WebSession> removeSessionIfExpired(ServerWebExchange exchange,
|
||||
WebSession session) {
|
||||
if (session.isExpired()) {
|
||||
this.sessionIdResolver.expireSession(exchange);
|
||||
return this.sessionStore.removeSession(session.getId()).then(Mono.empty());
|
||||
}
|
||||
return Mono.just(session);
|
||||
}
|
||||
|
||||
private Mono<Void> saveSession(ServerWebExchange exchange, WebSession session) {
|
||||
if (session.isExpired()) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Sessions are checked for expiration and have their "
|
||||
+ "lastAccessTime updated when first accessed during request processing. "
|
||||
+ "However this session is expired meaning that maxIdleTime elapsed "
|
||||
+ "before the call to session.save()."));
|
||||
}
|
||||
|
||||
if (!session.isStarted()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Force explicit start
|
||||
session.start();
|
||||
|
||||
if (hasNewSessionId(exchange, session)) {
|
||||
this.sessionIdResolver.setSessionId(exchange, session.getId());
|
||||
}
|
||||
|
||||
return this.sessionStore.storeSession(session);
|
||||
}
|
||||
|
||||
private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) {
|
||||
List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
|
||||
return ids.isEmpty() || !session.getId().equals(ids.get(0));
|
||||
}
|
||||
|
||||
private Mono<WebSession> createSession(ServerWebExchange exchange) {
|
||||
return this.sessionStore.createSession();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package org.springframework.session.web.server.session;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.AbstractCollection;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collection;
|
||||
@@ -27,7 +29,6 @@ import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -47,28 +48,42 @@ import org.springframework.web.server.session.WebSessionStore;
|
||||
* @author Rob Winch
|
||||
* @since 2.0
|
||||
*/
|
||||
class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore {
|
||||
public class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore {
|
||||
|
||||
private final ReactorSessionRepository<S> sessions;
|
||||
|
||||
SpringSessionWebSessionStore(ReactorSessionRepository<S> sessions) {
|
||||
Assert.notNull(sessions, "sessions cannot be null");
|
||||
this.sessions = sessions;
|
||||
private Clock clock = Clock.system(ZoneOffset.UTC);
|
||||
|
||||
public SpringSessionWebSessionStore(ReactorSessionRepository<S> reactorSessionRepository) {
|
||||
Assert.notNull(reactorSessionRepository, "reactorSessionRepository cannot be null");
|
||||
this.sessions = reactorSessionRepository;
|
||||
}
|
||||
|
||||
public Mono<WebSession> createSession() {
|
||||
/**
|
||||
* Configure the {@link Clock} to use to set lastAccessTime on every created
|
||||
* session and to calculate if it is expired.
|
||||
* <p>This may be useful to align to different timezone or to set the clock
|
||||
* back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
|
||||
* in order to simulate session expiration.
|
||||
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
|
||||
* @param clock the clock to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock cannot be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Mono<WebSession> createWebSession() {
|
||||
return this.sessions.createSession().map(this::createSession);
|
||||
}
|
||||
|
||||
public Mono<WebSession> setLastAccessedTime(WebSession session,
|
||||
Instant lastAccessedTime) {
|
||||
public Mono<WebSession> updateLastAccessTime(WebSession session) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SpringSessionWebSession springSessionWebSession = (SpringSessionWebSession) session;
|
||||
springSessionWebSession.session.setLastAccessedTime(lastAccessedTime);
|
||||
springSessionWebSession.session.setLastAccessedTime(this.clock.instant());
|
||||
return Mono.just(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> storeSession(WebSession session) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SpringSessionWebSession springWebSession = (SpringSessionWebSession) session;
|
||||
@@ -81,8 +96,8 @@ class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> changeSessionId(String s, WebSession webSession) {
|
||||
return storeSession(webSession);
|
||||
public Mono<Void> removeSession(String sessionId) {
|
||||
return this.sessions.deleteById(sessionId);
|
||||
}
|
||||
|
||||
private SpringSessionWebSession createSession(S session) {
|
||||
@@ -93,9 +108,89 @@ class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore
|
||||
return new SpringSessionWebSession(session, State.STARTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeSession(String sessionId) {
|
||||
return this.sessions.delete(sessionId);
|
||||
/**
|
||||
* Adapts Spring Session's {@link Session} to a {@link WebSession}.
|
||||
*/
|
||||
private class SpringSessionWebSession implements WebSession {
|
||||
|
||||
private final S session;
|
||||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
private AtomicReference<State> state = new AtomicReference<>();
|
||||
|
||||
SpringSessionWebSession(S session, State state) {
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
this.session = session;
|
||||
this.attributes = new SpringSessionMap(session);
|
||||
this.state.set(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.session.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> changeSessionId() {
|
||||
return Mono.defer(() -> {
|
||||
this.session
|
||||
.changeSessionId();
|
||||
return save();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.state.compareAndSet(State.NEW, State.STARTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
State value = this.state.get();
|
||||
return (State.STARTED.equals(value)
|
||||
|| (State.NEW.equals(value) && !getAttributes().isEmpty()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
return SpringSessionWebSessionStore.this.sessions.deleteById(this.session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save() {
|
||||
return SpringSessionWebSessionStore.this.sessions.save(this.session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.session.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.session.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return this.session.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return this.session.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
this.session.setMaxInactiveInterval(maxIdleTime);
|
||||
}
|
||||
}
|
||||
|
||||
private enum State {
|
||||
@@ -236,89 +331,5 @@ class SpringSessionWebSessionStore<S extends Session> implements WebSessionStore
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts Spring Session's {@link Session} to a {@link WebSession}.
|
||||
*/
|
||||
private class SpringSessionWebSession implements WebSession {
|
||||
|
||||
private final S session;
|
||||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
private AtomicReference<State> state = new AtomicReference<>();
|
||||
|
||||
private volatile transient Supplier<Mono<Void>> saveOperation = Mono::empty;
|
||||
|
||||
SpringSessionWebSession(S session, State state) {
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
this.session = session;
|
||||
this.attributes = new SpringSessionMap(session);
|
||||
this.state.set(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.session.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> changeSessionId() {
|
||||
return Mono.defer(() -> {
|
||||
this.session.changeSessionId();
|
||||
return save();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.state.compareAndSet(State.NEW, State.STARTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
State value = this.state.get();
|
||||
return (State.STARTED.equals(value)
|
||||
|| (State.NEW.equals(value) && !getAttributes().isEmpty()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save() {
|
||||
return this.saveOperation.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.session.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.session.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return this.session.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return this.session.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
this.session.setMaxInactiveInterval(maxIdleTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.springframework.session;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -29,50 +29,23 @@ import org.junit.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MapReactorSessionRepository}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class MapReactorSessionRepositoryTests {
|
||||
MapReactorSessionRepository repository;
|
||||
|
||||
MapSession session;
|
||||
private MapReactorSessionRepository repository;
|
||||
|
||||
private MapSession session;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.repository = new MapReactorSessionRepository();
|
||||
this.repository = new MapReactorSessionRepository(new HashMap<>());
|
||||
this.session = new MapSession("session-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorVarargsThenFound() {
|
||||
this.repository = new MapReactorSessionRepository(this.session);
|
||||
|
||||
Session findByIdSession = this.repository.findById(this.session.getId()).block();
|
||||
assertThat(findByIdSession).isNotNull();
|
||||
assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorVarargsWhenNullThenThrowsIllegalArgumentException() {
|
||||
Session[] sessions = null;
|
||||
new MapReactorSessionRepository(sessions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorIterableThenFound() {
|
||||
this.repository = new MapReactorSessionRepository(Arrays.asList(this.session));
|
||||
|
||||
Session findByIdSession = this.repository.findById(this.session.getId()).block();
|
||||
assertThat(findByIdSession).isNotNull();
|
||||
assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorIterableWhenNullThenThrowsIllegalArgumentException() {
|
||||
Iterable<Session> sessions = null;
|
||||
new MapReactorSessionRepository(sessions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorMapThenFound() {
|
||||
Map<String, Session> sessions = new HashMap<>();
|
||||
@@ -107,21 +80,16 @@ public class MapReactorSessionRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenNotExpiredThenFound() {
|
||||
this.repository = new MapReactorSessionRepository(this.session);
|
||||
|
||||
Session findByIdSession = this.repository.findById(this.session.getId()).block();
|
||||
assertThat(findByIdSession).isNotNull();
|
||||
assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdWhenExpiredThenEmpty() {
|
||||
this.session.setMaxInactiveInterval(Duration.ofSeconds(1));
|
||||
public void findByIdWhenExpiredRemovesFromSessionMap() {
|
||||
this.session.setMaxInactiveInterval(Duration.ofMinutes(1));
|
||||
this.session.setLastAccessedTime(Instant.now().minus(5, ChronoUnit.MINUTES));
|
||||
this.repository = new MapReactorSessionRepository(Arrays.asList(this.session));
|
||||
|
||||
Map<String, Session> sessions = new ConcurrentHashMap<>();
|
||||
sessions.put("session-id", this.session);
|
||||
this.repository = new MapReactorSessionRepository(sessions);
|
||||
|
||||
assertThat(this.repository.findById(this.session.getId()).block()).isNull();
|
||||
assertThat(sessions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,4 +141,5 @@ public class MapReactorSessionRepositoryTests {
|
||||
assertThat(this.repository.findById(originalId).block()).isNull();
|
||||
assertThat(this.repository.findById(createSession.getId()).block()).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,20 +19,25 @@ package org.springframework.session;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MapSessionRepository}.
|
||||
*/
|
||||
public class MapSessionRepositoryTests {
|
||||
MapSessionRepository repository;
|
||||
|
||||
MapSession session;
|
||||
private MapSessionRepository repository;
|
||||
|
||||
private MapSession session;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.repository = new MapSessionRepository();
|
||||
this.repository = new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
this.session = new MapSession();
|
||||
}
|
||||
|
||||
@@ -94,4 +99,5 @@ public class MapSessionRepositoryTests {
|
||||
assertThat(this.repository.findById(originalId)).isNull();
|
||||
assertThat(this.repository.findById(createSession.getId())).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.util.HashMap;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.UnsatisfiedDependencyException;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.session.CookieWebSessionIdResolver;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.HeaderWebSessionIdResolver;
|
||||
import org.springframework.web.server.session.WebSessionIdResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Verify various configurations through {@link EnableSpringWebSession}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class SpringWebSessionConfigurationTests {
|
||||
private AnnotationConfigApplicationContext context;
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableSpringWebSessionConfiguresThings() {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(GoodConfig.class);
|
||||
this.context.refresh();
|
||||
|
||||
WebSessionManager webSessionManagerFoundByType = this.context.getBean(WebSessionManager.class);
|
||||
Object webSessionManagerFoundByName = this.context.getBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME);
|
||||
|
||||
assertThat(webSessionManagerFoundByType).isNotNull();
|
||||
assertThat(webSessionManagerFoundByName).isNotNull();
|
||||
assertThat(webSessionManagerFoundByType).isEqualTo(webSessionManagerFoundByName);
|
||||
|
||||
assertThat(this.context.getBean(ReactorSessionRepository.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingReactorSessionRepositoryBreaksAppContext() {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(BadConfig.class);
|
||||
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(this.context::refresh)
|
||||
.withMessageContaining("Error creating bean with name 'webSessionManager'")
|
||||
.withMessageContaining("No qualifying bean of type '" + ReactorSessionRepository.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultSessionIdResolverShouldBeCookieBased() {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(GoodConfig.class);
|
||||
this.context.refresh();
|
||||
|
||||
DefaultWebSessionManager manager = this.context.getBean(DefaultWebSessionManager.class);
|
||||
assertThat(manager.getSessionIdResolver().getClass()).isAssignableFrom(CookieWebSessionIdResolver.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void providedSessionIdResolverShouldBePickedUpAutomatically() {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(OverrideSessionIdResolver.class);
|
||||
this.context.refresh();
|
||||
|
||||
DefaultWebSessionManager manager = this.context.getBean(DefaultWebSessionManager.class);
|
||||
assertThat(manager.getSessionIdResolver().getClass()).isAssignableFrom(HeaderWebSessionIdResolver.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration with all the right parts.
|
||||
*/
|
||||
@EnableSpringWebSession
|
||||
static class GoodConfig {
|
||||
|
||||
/**
|
||||
* Use Reactor-friendly, {@link java.util.Map}-backed {@link ReactorSessionRepository} for test purposes.
|
||||
*/
|
||||
@Bean
|
||||
ReactorSessionRepository<?> reactorSessionRepository() {
|
||||
return new MapReactorSessionRepository(new HashMap<>());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration where no {@link ReactorSessionRepository} is defined. It's BAD!
|
||||
*/
|
||||
@EnableSpringWebSession
|
||||
static class BadConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableSpringWebSession
|
||||
static class OverrideSessionIdResolver {
|
||||
|
||||
@Bean
|
||||
ReactorSessionRepository<?> reactorSessionRepository() {
|
||||
return new MapReactorSessionRepository(new HashMap<>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebSessionIdResolver alternateWebSessionIdResolver() {
|
||||
return new HeaderWebSessionIdResolver();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.session.config.annotation.web.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
@@ -50,25 +51,29 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* Tests for {@link SpringHttpSessionConfiguration} using a custom
|
||||
* {@link CookieSerializer}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
public class EnableSpringHttpSessionCustomCookieSerializerTests {
|
||||
@Autowired
|
||||
MockHttpServletRequest request;
|
||||
@Autowired
|
||||
MockHttpServletResponse response;
|
||||
|
||||
MockFilterChain chain;
|
||||
|
||||
@Autowired
|
||||
SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
CookieSerializer cookieSerializer;
|
||||
private MockHttpServletResponse response;
|
||||
|
||||
private MockFilterChain chain;
|
||||
|
||||
@Autowired
|
||||
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
|
||||
|
||||
@Autowired
|
||||
private CookieSerializer cookieSerializer;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
@@ -109,14 +114,17 @@ public class EnableSpringHttpSessionCustomCookieSerializerTests {
|
||||
@EnableSpringHttpSession
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public MapSessionRepository mapSessionRepository() {
|
||||
return new MapSessionRepository();
|
||||
return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CookieSerializer cookieSerializer() {
|
||||
return mock(CookieSerializer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.session.config.annotation.web.http;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -43,25 +45,29 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* Tests for {@link SpringHttpSessionConfiguration} using a custom
|
||||
* {@link MultiHttpSessionStrategy}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
public class EnableSpringHttpSessionCustomMultiHttpSessionStrategyTests {
|
||||
@Autowired
|
||||
MockHttpServletRequest request;
|
||||
@Autowired
|
||||
MockHttpServletResponse response;
|
||||
|
||||
MockFilterChain chain;
|
||||
|
||||
@Autowired
|
||||
SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
MultiHttpSessionStrategy strategy;
|
||||
private MockHttpServletResponse response;
|
||||
|
||||
private MockFilterChain chain;
|
||||
|
||||
@Autowired
|
||||
private SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
|
||||
|
||||
@Autowired
|
||||
private MultiHttpSessionStrategy strategy;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
@@ -86,14 +92,17 @@ public class EnableSpringHttpSessionCustomMultiHttpSessionStrategyTests {
|
||||
@EnableSpringHttpSession
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public MapSessionRepository mapSessionRepository() {
|
||||
return new MapSessionRepository();
|
||||
return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MultiHttpSessionStrategy strategy() {
|
||||
return mock(MultiHttpSessionStrategy.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.session.config.annotation.web.http;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.junit.After;
|
||||
@@ -134,7 +136,7 @@ public class SpringHttpSessionConfigurationTests {
|
||||
|
||||
@Bean
|
||||
public MapSessionRepository sessionRepository() {
|
||||
return new MapSessionRepository();
|
||||
return new MapSessionRepository(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -343,6 +343,16 @@ public class DefaultCookieSerializerTests {
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeCookieCookieMaxAgeExplicitCookieValue() {
|
||||
CookieValue cookieValue = cookieValue(this.sessionId);
|
||||
cookieValue.setCookieMaxAge(100);
|
||||
|
||||
this.serializer.writeCookieValue(cookieValue);
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
}
|
||||
|
||||
// --- secure ---
|
||||
|
||||
@Test
|
||||
@@ -443,6 +453,17 @@ public class DefaultCookieSerializerTests {
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeCookieRememberMeCookieMaxAgeOverride() {
|
||||
this.request.setAttribute("rememberMe", true);
|
||||
this.serializer.setRememberMeRequestAttribute("rememberMe");
|
||||
CookieValue cookieValue = cookieValue(this.sessionId);
|
||||
cookieValue.setCookieMaxAge(100);
|
||||
this.serializer.writeCookieValue(cookieValue);
|
||||
|
||||
assertThat(getCookie().getMaxAge()).isEqualTo(100);
|
||||
}
|
||||
|
||||
public void setCookieName(String cookieName) {
|
||||
this.cookieName = cookieName;
|
||||
this.serializer.setCookieName(cookieName);
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -68,9 +69,13 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link SessionRepositoryFilter}.
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SessionRepositoryFilterTests {
|
||||
|
||||
@Mock
|
||||
private HttpSessionStrategy strategy;
|
||||
|
||||
@@ -420,7 +425,7 @@ public class SessionRepositoryFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterSetsCookieIfChanged() throws Exception {
|
||||
this.sessionRepository = new MapSessionRepository() {
|
||||
this.sessionRepository = new MapSessionRepository(new ConcurrentHashMap<>()) {
|
||||
@Override
|
||||
public MapSession findById(String id) {
|
||||
return createSession();
|
||||
@@ -1256,7 +1261,8 @@ public class SessionRepositoryFilterTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void doFilterRequestSessionNoRequestSessionNoSessionRepositoryInteractions()
|
||||
throws Exception {
|
||||
SessionRepository<MapSession> sessionRepository = spy(new MapSessionRepository());
|
||||
SessionRepository<MapSession> sessionRepository = spy(
|
||||
new MapSessionRepository(new ConcurrentHashMap<>()));
|
||||
|
||||
this.filter = new SessionRepositoryFilter<>(sessionRepository);
|
||||
|
||||
@@ -1283,7 +1289,8 @@ public class SessionRepositoryFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterLazySessionCreation() throws Exception {
|
||||
SessionRepository<MapSession> sessionRepository = spy(new MapSessionRepository());
|
||||
SessionRepository<MapSession> sessionRepository = spy(
|
||||
new MapSessionRepository(new ConcurrentHashMap<>()));
|
||||
|
||||
this.filter = new SessionRepositoryFilter<>(sessionRepository);
|
||||
|
||||
@@ -1480,4 +1487,5 @@ public class SessionRepositoryFilterTests {
|
||||
return SessionRepositoryFilter.DEFAULT_ORDER;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* 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.web.server.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.session.ReactorSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.WebSessionIdResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link SpringSessionWebSessionManager}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SpringSessionWebSessionManagerTests<S extends Session> {
|
||||
|
||||
@Mock
|
||||
private ReactorSessionRepository<S> sessions;
|
||||
|
||||
@Mock
|
||||
private WebSessionIdResolver resolver;
|
||||
|
||||
@Mock
|
||||
private S createSession;
|
||||
|
||||
@Mock
|
||||
private S findByIdSession;
|
||||
|
||||
private Mono<S> createSessionMono;
|
||||
|
||||
private ServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange();
|
||||
|
||||
private SpringSessionWebSessionManager manager;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
given(this.createSession.getId()).willReturn("createSession-id");
|
||||
given(this.findByIdSession.getId()).willReturn("findByIdSession-id");
|
||||
this.createSessionMono = Mono.just(this.createSession);
|
||||
given(this.sessions.createSession()).willReturn(this.createSessionMono);
|
||||
this.manager = new SpringSessionWebSessionManager(this.sessions);
|
||||
this.manager.setSessionIdResolver(this.resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionWhenDefaultSessionIdResolverFoundSessionUsed() {
|
||||
String findByIdSessionId = this.findByIdSession.getId();
|
||||
this.exchange = MockServerHttpRequest.get("/")
|
||||
.cookie(new HttpCookie("SESSION", findByIdSessionId)).toExchange();
|
||||
this.manager = new SpringSessionWebSessionManager(this.sessions);
|
||||
given(this.sessions.findById(findByIdSessionId))
|
||||
.willReturn(Mono.just(this.findByIdSession));
|
||||
|
||||
WebSession webSession = this.manager.getSession(this.exchange).block();
|
||||
|
||||
assertThat(webSession.getId()).isEqualTo(findByIdSessionId);
|
||||
verify(this.sessions).findById(findByIdSessionId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionWhenNewThenCreateSessionInvoked() {
|
||||
WebSession webSession = this.manager.getSession(this.exchange).block();
|
||||
|
||||
assertThat(webSession.getId()).isEqualTo(this.createSession.getId());
|
||||
verify(this.sessions).createSession();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionWhenNewAndPutThenSetAttributeInvoked() {
|
||||
String attrName = "attrName";
|
||||
String attrValue = "attrValue";
|
||||
|
||||
WebSession webSession = this.manager.getSession(this.exchange).block();
|
||||
webSession.getAttributes().put(attrName, attrValue);
|
||||
|
||||
verify(this.createSession).setAttribute(attrName, attrValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionWhenInvalidIdThenCreateSessionInvoked() {
|
||||
String invalidId = "invalid";
|
||||
String createSessionId = this.createSession.getId();
|
||||
given(this.sessions.findById(any())).willReturn(Mono.empty());
|
||||
given(this.resolver.resolveSessionIds(this.exchange))
|
||||
.willReturn(Collections.singletonList(invalidId));
|
||||
|
||||
WebSession webSession = this.manager.getSession(this.exchange).block();
|
||||
|
||||
assertThat(webSession.getId()).isEqualTo(createSessionId);
|
||||
verify(this.sessions).findById(invalidId);
|
||||
|
||||
Mono<String> mono = Mono.just("toTest");
|
||||
StepVerifier.create(mono).expectNoEvent(Duration.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionWhenValidIdThenFoundSessionUsed() {
|
||||
String findByIdSessionId = this.findByIdSession.getId();
|
||||
given(this.sessions.findById(findByIdSessionId))
|
||||
.willReturn(Mono.just(this.findByIdSession));
|
||||
given(this.resolver.resolveSessionIds(this.exchange))
|
||||
.willReturn(Arrays.asList(findByIdSessionId));
|
||||
|
||||
WebSession webSession = this.manager.getSession(this.exchange).block();
|
||||
|
||||
assertThat(webSession.getId()).isEqualTo(findByIdSessionId);
|
||||
verify(this.sessions).findById(findByIdSessionId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,7 +73,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenNoAttributesThenNotStarted() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
assertThat(createdWebSession.isStarted()).isFalse();
|
||||
}
|
||||
@@ -82,14 +83,16 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
public void createSessionWhenAddAttributeThenStarted() {
|
||||
given(this.createSession.getAttributeNames())
|
||||
.willReturn(Collections.singleton("a"));
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
assertThat(createdWebSession.isStarted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndSizeThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -103,7 +106,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndIsEmptyThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -117,7 +121,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndContainsKeyAndNotStringThenFalse() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -126,7 +131,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndContainsKeyAndNotFoundThenFalse() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -137,7 +143,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
public void createSessionWhenGetAttributesAndContainsKeyAndFoundThenTrue() {
|
||||
given(this.createSession.getAttributeNames())
|
||||
.willReturn(Collections.singleton("a"));
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -146,7 +153,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndPutThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
attributes.put("a", "b");
|
||||
@@ -156,7 +164,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndPutNullThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
attributes.put("a", null);
|
||||
@@ -166,7 +175,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndRemoveThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
attributes.remove("a");
|
||||
@@ -176,7 +186,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
|
||||
@Test
|
||||
public void createSessionWhenGetAttributesAndPutAllThenDelegatesToCreateSession() {
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
attributes.putAll(Collections.singletonMap("a", "b"));
|
||||
@@ -188,7 +199,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
public void createSessionWhenGetAttributesAndClearThenDelegatesToCreateSession() {
|
||||
given(this.createSession.getAttributeNames())
|
||||
.willReturn(Collections.singleton("a"));
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
attributes.clear();
|
||||
@@ -200,7 +212,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
public void createSessionWhenGetAttributesAndKeySetThenDelegatesToCreateSession() {
|
||||
given(this.createSession.getAttributeNames())
|
||||
.willReturn(Collections.singleton("a"));
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -212,7 +225,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
given(this.createSession.getAttributeNames())
|
||||
.willReturn(Collections.singleton("a"));
|
||||
given(this.createSession.getAttribute("a")).willReturn("b");
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
|
||||
@@ -226,7 +240,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
.willReturn(Collections.singleton(attrName));
|
||||
String attrValue = "attrValue";
|
||||
given(this.createSession.getAttribute(attrName)).willReturn(attrValue);
|
||||
WebSession createdWebSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdWebSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
Map<String, Object> attributes = createdWebSession.getAttributes();
|
||||
Set<Map.Entry<String, Object>> entries = attributes.entrySet();
|
||||
@@ -238,7 +253,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
@Test
|
||||
public void storeSessionWhenInvokedThenSessionSaved() {
|
||||
given(this.sessionRepository.save(this.createSession)).willReturn(Mono.empty());
|
||||
WebSession createdSession = this.webSessionStore.createSession().block();
|
||||
WebSession createdSession = this.webSessionStore.createWebSession()
|
||||
.block();
|
||||
|
||||
this.webSessionStore.storeSession(createdSession).block();
|
||||
|
||||
@@ -248,7 +264,8 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
@Test
|
||||
public void retrieveSessionThenStarted() {
|
||||
String id = "id";
|
||||
WebSession retrievedWebSession = this.webSessionStore.retrieveSession(id).block();
|
||||
WebSession retrievedWebSession = this.webSessionStore
|
||||
.retrieveSession(id).block();
|
||||
|
||||
assertThat(retrievedWebSession.isStarted()).isTrue();
|
||||
}
|
||||
@@ -256,11 +273,15 @@ public class SpringSessionWebSessionStoreTests<S extends Session> {
|
||||
@Test
|
||||
public void removeSessionWhenInvokedThenSessionSaved() {
|
||||
String sessionId = "session-id";
|
||||
given(this.sessionRepository.delete(sessionId)).willReturn(Mono.empty());
|
||||
given(this.sessionRepository.deleteById(sessionId)).willReturn(Mono.empty());
|
||||
|
||||
this.webSessionStore.removeSession(sessionId).block();
|
||||
|
||||
verify(this.sessionRepository).delete(sessionId);
|
||||
verify(this.sessionRepository).deleteById(sessionId);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void setClockWhenNullThenException() {
|
||||
this.webSessionStore.setClock(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ dependencies {
|
||||
exclude group: "org.slf4j", module: 'jcl-over-slf4j'
|
||||
}
|
||||
|
||||
optional "io.projectreactor:reactor-core"
|
||||
optional "org.springframework:spring-web"
|
||||
|
||||
testCompile "io.projectreactor:reactor-test"
|
||||
testCompile "javax.servlet:javax.servlet-api"
|
||||
testCompile "org.springframework:spring-web"
|
||||
testCompile "org.springframework.security:spring-security-core"
|
||||
|
||||
integrationTestCompile "redis.clients:jedis"
|
||||
integrationTestCompile "org.apache.commons:commons-pool2"
|
||||
integrationTestCompile "io.lettuce:lettuce-core"
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user