Compare commits

..

28 Commits

Author SHA1 Message Date
Spring Buildmaster
f9163d94fd Release version 1.2.0.RC2 2016-04-06 14:26:36 +00:00
Vedran Pavić
3f819a94b1 Enable transaction management for JdbcOperationsSessionRepository operations 2016-04-05 23:39:36 -05:00
Rob Winch
1e1d24895c Merge pull request #454 from vpavic/jdbc-boot-sample
Add JDBC Spring Boot sample
2016-04-04 08:36:27 -05:00
Rob Winch
e134a4cdb8 Merge pull request #463 from vpavic/fix-h2-console-urls
Make H2 console URL consistent across sample projects
2016-04-04 07:35:45 -05:00
Rob Winch
75006cd7dd Merge pull request #462 from vpavic/gh-457
Update WebSocket sample to use H2 console auto-configuration
2016-04-04 07:34:09 -05:00
Vedran Pavic
9c8f8894e1 Add JDBC Spring Boot sample 2016-04-03 20:57:16 +02:00
Vedran Pavic
99db45ea72 Make H2 console URL consistent across sample projects 2016-04-01 22:36:24 +02:00
Vedran Pavic
c07583bd47 Update WebSocket sample to use H2 console auto-configuration
Fixes gh-457.
2016-04-01 22:26:13 +02:00
Rob Winch
cce8dac4b7 Fix WebSocket AbstractMethodError
Fixes gh-460
2016-04-01 14:27:08 -05:00
Rob Winch
5bde226ecc Fix Eclipse compile errors
* Most web.xml servlet API versions updated to 3.0 for ASYNC support
* httpsession-xml is left at 2.5 to ensure compatability & remove ASYNC
* Remove @Override on interface override
2016-04-01 11:56:27 -05:00
Rob Winch
f8f6ee20c0 Externalize sample.gradle 2016-03-30 10:23:11 -05:00
Rob Winch
3bb96e8e82 Remove Wrapper from Gradle Sample
Issue gh-246
2016-03-30 10:22:59 -05:00
Rob Winch
b7367680cb Fix Package Names in Grails Sample
Issue gh-246
2016-03-30 10:22:59 -05:00
Rob Winch
a26a21b663 Add Grails 3 Sample to What's New
Issue gh-246
2016-03-30 09:28:03 -05:00
Rob Winch
3825a46418 Polish Grails 3 Sample
Issue gh-246
2016-03-29 20:58:26 -05:00
Eric Helgeson
779277b16d Add Grails Sample
Fixes gh-246
2016-03-29 20:58:26 -05:00
Rob Winch
b97306a83d Merge pull request #450 from vpavic/gh-445
Fix loading of JdbcSession's lastAccessedTime attribute
2016-03-28 15:06:49 -05:00
Rob Winch
6b13111079 Merge pull request #451 from vpavic/polish-tests
Polish JdbcHttpSessionConfigurationTests
2016-03-28 11:15:15 -05:00
Vedran Pavic
63006db45d Fix loading of JdbcSession's lastAccessedTime attribute
Fixes gh-445.
2016-03-28 18:13:16 +02:00
Vedran Pavic
0a99e065ff Polish JdbcHttpSessionConfigurationTests 2016-03-28 17:49:02 +02:00
Scott Carlson
bd2d846917 Add Dispatcher types to web.xml
Fixes gh-443
2016-03-28 09:16:13 -05:00
Rob Winch
79928bd7fe Merge pull request #436 from vpavic/improve-mongo-it
Use Flapdoodle Embedded MongoDB for integration tests
2016-03-25 09:30:40 -05:00
Vedran Pavic
903cac492e Use Flapdoodle Embedded MongoDB for integration tests and samples 2016-03-25 07:27:33 +01:00
Rob Winch
3980be349b Merge pull request #440 from lowzj/master_fix-typo
Fix SessionRepositoryFilter comment typo
2016-03-23 08:27:23 -05:00
lowzj
128e0c4d47 Fix SessionRepositoryFilter comment typo 2016-03-23 19:32:05 +08:00
Rob Winch
37bc6a352f Merge pull request #437 from vpavic/improve-build
Externalize H2 database dependency version
2016-03-20 11:10:24 -05:00
Vedran Pavic
b88f48f01d Externalize H2 database dependency version 2016-03-20 01:56:35 +01:00
Spring Buildmaster
028e277fc9 Next development version 2016-03-16 20:32:18 -07:00
82 changed files with 2112 additions and 224 deletions

View File

@@ -2,7 +2,6 @@ language: java
services:
- redis-server
- mongodb
jdk:
- oraclejdk8
@@ -21,4 +20,4 @@ cache:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
script: ./gradlew build
script: ./gradlew build

View File

@@ -23,6 +23,7 @@ ext.IDE_GRADLE = "$rootDir/gradle/ide.gradle"
ext.JAVA_GRADLE = "$rootDir/gradle/java.gradle"
ext.SPRING3_GRADLE = "$rootDir/gradle/spring3.gradle"
ext.MAVEN_GRADLE = "$rootDir/gradle/publish-maven.gradle"
ext.SAMPLE_GRADLE = "$rootDir/gradle/sample.gradle"
ext.TOMCAT_GRADLE = "$rootDir/gradle/tomcat.gradle"
ext.TOMCAT_6_GRADLE = "$rootDir/gradle/tomcat6.gradle"
ext.TOMCAT_7_GRADLE = "$rootDir/gradle/tomcat7.gradle"

View File

@@ -0,0 +1,129 @@
= Spring Session - Grails
Eric Helgeson
:toc:
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Grails 3.1
NOTE: Grails 3.1 is based off spring boot 1.3 so much of the advanced configuration and options can be found in the boot docs as well.
NOTE: The completed guide can be found in the <<grails3-sample, Grails 3 sample application>>.
== Updating Dependencies
Before you use Spring Session, you must ensure to update your dependencies.
We assume you are working with a working Grails 3.1 web profile.
Add the following dependencies:
.build.gradle
[source,groovy]
[subs="verbatim,attributes"]
----
dependencies {
compile 'org.springframework.boot:spring-boot-starter-redis'
compile 'org.springframework.session:spring-session:{spring-session-version}'
}
----
ifeval::["{version-snapshot}" == "true"]
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
Ensure you have the following in your pom.xml:
.build.gradle
[source,groovy]
----
repositories {
maven {
url 'https://repo.spring.io/libs-snapshot'
}
}
----
endif::[]
ifeval::["{version-milestone}" == "true"]
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
Ensure you have the following in your pom.xml:
.build.gradle
[source,groovy]
----
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
----
endif::[]
[[grails3-redis-configuration]]
== Configuring the Redis Connection
Spring Boot automatically creates a `RedisConnectionFactory` that connects Spring Session to a Redis Server on localhost on port 6379 (default port).
In a production environment you need to ensure to update your configuration to point to your Redis server.
For example, you can include the following in your *application.yml*
.grails-app/conf/application.yml
[source,yml]
----
spring:
redis:
host: localhost
password: secret
port: 6397
----
For more information, refer to http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation.
[[grails3-sample]]
== Grails 3 Sample Application
The Grails 3 Sample Application demonstrates how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Grails.
[[grails3-running]]
=== Running the Grails 3 Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
[NOTE]
====
For the sample to work, you must 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.
====
----
$ ./gradlew :samples:grails3:bootRun
----
You should now be able to access the application at http://localhost:8080/test/index
[[grails3-explore]]
=== Exploring the security Sample Application
Try using the application. Enter the following to log in:
* **Username** _user_
* **Password** _password_
Now click the **Login** button.
You should now see a message indicating your are logged in with the user entered previously.
The user's information is stored in Redis rather than Tomcat's `HttpSession` implementation.
[[grails3-how]]
=== How does it work?
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
Spring Session replaces the `HttpSession` with an implementation that is backed by Redis.
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Redis.
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type:
$ redis-cli keys '*' | xargs redis-cli del
TIP: The Redis documentation has instructions for http://redis.io/topics/quickstart[installing redis-cli].
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie:
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
Now visit the application at http://localhost:8080/test/index and observe that we are no longer authenticated.

View File

@@ -0,0 +1,152 @@
= Spring Session - Spring Boot
Rob Winch, Vedran Pavić
:toc:
This guide describes how to use Spring Session to transparently leverage a relational database to back a web application's `HttpSession` when using Spring Boot.
NOTE: The completed guide can be found in the <<httpsession-jdbc-boot-sample, httpsession-jdbc-boot sample application>>.
== Updating Dependencies
Before you use Spring Session, you must ensure to update your dependencies.
We assume you are working with a working Spring Boot web application.
If you are using Maven, ensure to add the following dependencies:
.pom.xml
[source,xml]
[subs="verbatim,attributes"]
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
<version>{spring-session-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
----
ifeval::["{version-snapshot}" == "true"]
Since we are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
Ensure you have the following in your pom.xml:
.pom.xml
[source,xml]
----
<repositories>
<!-- ... -->
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
----
endif::[]
ifeval::["{version-milestone}" == "true"]
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
Ensure you have the following in your pom.xml:
.pom.xml
[source,xml]
----
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
----
endif::[]
// tag::config[]
[[httpsession-jdbc-boot-spring-configuration]]
== Spring Configuration
After adding the required dependencies, we can create our Spring configuration.
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
Add the following Spring Configuration:
[source,java]
----
include::{samples-dir}httpsession-jdbc-boot/src/main/java/sample/config/HttpSessionConfig.java[tags=class]
----
<1> The `@EnableJdbcHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
In this instance Spring Session is backed by a relational database.
[[httpsession-jdbc-boot-configuration]]
== Configuring the DataSource
Spring Boot automatically creates a `DataSource` that connects Spring Session to an embedded instance of H2 database.
In a production environment you need to ensure to update your configuration to point to your relational database.
For example, you can include the following in your *application.properties*
.src/main/resources/application.properties
----
spring.datasource.url=jdbc:postgresql://localhost:5432/myapp
spring.datasource.username=myapp
spring.datasource.password=secret
----
For more information, refer to http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-configure-datasource[Configure a DataSource] portion of the Spring Boot documentation.
[[httpsession-jdbc-boot-servlet-configuration]]
== Servlet Container Initialization
Our <<httpsession-jdbc-boot-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
Fortunately, Spring Boot takes care of both of these steps for us.
// end::config[]
[[httpsession-jdbc-boot-sample]]
== httpsession-jdbc-boot Sample Application
The httpsession-jdbc-boot Sample Application demonstrates how to use Spring Session to transparently leverage H2 database to back a web application's `HttpSession` when using Spring Boot.
[[httpsession-jdbc-boot-running]]
=== Running the httpsession-jdbc-boot Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
----
$ ./gradlew :samples:httpsession-jdbc-boot:bootRun
----
You should now be able to access the application at http://localhost:8080/
[[httpsession-jdbc-boot-explore]]
=== Exploring the security Sample Application
Try using the application. Enter the following to log in:
* **Username** _user_
* **Password** _password_
Now click the **Login** button.
You should now see a message indicating your are logged in with the user entered previously.
The user's information is stored in H2 database rather than Tomcat's `HttpSession` implementation.
[[httpsession-jdbc-boot-how]]
=== How does it work?
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in H2 database.
Spring Session replaces the `HttpSession` with an implementation that is backed by a relational database.
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into H2 database.
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.

View File

@@ -85,6 +85,7 @@ The filter is what is in charge of replacing the `HttpSession` implementation to
In this instance Spring Session is backed by a relational database.
<2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database.
We configure the H2 database to create database tables using the SQL script which is included in Spring Session.
<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`.
== XML Servlet Container Initialization
@@ -154,6 +155,6 @@ Instead of using Tomcat's `HttpSession`, we are actually persisting the values i
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/console (use `jdbc:h2:mem:testdb` for JDBC URL)
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.

View File

@@ -83,6 +83,7 @@ The filter is what is in charge of replacing the `HttpSession` implementation to
In this instance Spring Session is backed by a relational database.
<2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database.
We configure the H2 database to create database tables using the SQL script which is included in Spring Session.
<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`.
== Java Servlet Container Initialization
@@ -144,6 +145,6 @@ Instead of using Tomcat's `HttpSession`, we are actually persisting the values i
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/console (use `jdbc:h2:mem:testdb` for JDBC URL)
If you like, you can easily remove the session using H2 web console available at: http://localhost:8080/h2-console/ (use `jdbc:h2:mem:testdb` for JDBC URL)
Now visit the application at http://localhost:8080/ and observe that the attribute we added is no longer displayed.

View File

@@ -122,12 +122,6 @@ The Mongo Sample Application demonstrates how to use Spring Session to transpare
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
[NOTE]
====
For the sample to work, you must have MongoDB on localhost and run it with the default port (27017).
Alternatively you can use docker to run local instance `docker run -p 27017:27017 mongo`
====
----
$ ./gradlew :samples:mongo:bootRun
----
@@ -156,9 +150,15 @@ When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityCon
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using mongo client. For example, on a Linux based system you can type:
If you like, you can easily inspect the session using mongo client. For example, on a Linux based system you can type:
$ mongo
[NOTE]
====
The sample application uses an embedded MongoDB instance that listens on a randomly allocated port.
The port used by embedded MongoDB together with exact command to connect to it is logged during application startup.
====
$ mongo --port ...
> use test
> db.sessions.find().pretty()

View File

@@ -29,6 +29,7 @@ Below are the highlights of what is new in Spring Session 1.2. You can find a co
* Added <<httpsession-jdbc,JdbcOperationsSessionRepository>> (See https://github.com/spring-projects/spring-session/issues/364[#364]).
* Added <<httpsession-mongo,MongoOperationsSessionRepository>> (See https://github.com/spring-projects/spring-session/pull/371[#371]).
* SessionRepositoryFilter caches null session lookup (See https://github.com/spring-projects/spring-session/issues/423[#423])
* link:guides/grails3.html[Grails 3 Sample & Guide]
* Improved Workspace Setup (See https://github.com/spring-projects/spring-session/pull/417[#417])
[[samples]]
@@ -72,6 +73,10 @@ If you are looking to get started with Spring Session, the best place to start i
| Demonstrates how to use Spring Session with Spring Boot.
| link:guides/boot.html[Spring Boot Guide]
| {gh-samples-url}grails3[Grails 3]
| Demonstrates how to use Spring Session with Grails 3.
| link:guides/grails3.html[Grails 3 Guide]
| {gh-samples-url}security[Spring Security]
| Demonstrates how to use Spring Session with an existing Spring Security application.
| link:guides/security.html[Spring Security Guide]
@@ -114,6 +119,10 @@ If you are looking to get started with Spring Session, the best place to start i
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store using XML based configuration.
| link:guides/httpsession-jdbc-xml.html[HttpSession JDBC XML Guide]
| {gh-samples-url}httpsession-jdbc-boot[HttpSession JDBC Spring Boot]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a relational database store when using Spring Boot.
| link:guides/httpsession-jdbc-boot.html[HttpSession JDBC Spring Boot Guide]
|===
[[httpsession]]
@@ -272,6 +281,7 @@ You can choose from enabling this using either:
* <<httpsession-jdbc-jc,Java Based Configuration>>
* <<httpsession-jdbc-xml,XML Based Configuration>>
* <<httpsession-jdbc-boot,Spring Boot Based Configuration>>
[[httpsession-jdbc-jc]]
==== JDBC Java Based Configuration
@@ -293,6 +303,16 @@ You can read the basic steps for integration below, but you are encouraged to fo
include::guides/httpsession-jdbc-xml.adoc[tags=config,leveloffset=+3]
[[httpsession-jdbc-boot]]
==== JDBC Spring Boot Based Configuration
This section describes how to use a relational database to back `HttpSession` when using Spring Boot.
NOTE: The <<samples, HttpSession JDBC Spring Boot Sample>> provides a working sample on how to integrate Spring Session and `HttpSession` using Spring Boot.
You can read the basic steps for integration below, but you are encouraged to follow along with the detailed HttpSession JDBC Spring Boot Guide when integrating with your own application.
include::guides/httpsession-jdbc-boot.adoc[tags=config,leveloffset=+3]
[[httpsession-mongo]]
=== HttpSession with Mongo
@@ -992,7 +1012,7 @@ A typical example of how to create a new instance can be seen below:
include::{indexdoc-tests}[tags=new-jdbcoperationssessionrepository]
----
For additional information on how to create and configure a `JdbcTemplate`, refer to the Spring Framework Reference Documentation.
For additional information on how to create and configure `JdbcTemplate` and `PlatformTransactionManager`, refer to the Spring Framework Reference Documentation.
[[api-jdbcoperationssessionrepository-config]]
==== EnableJdbcHttpSession
@@ -1036,6 +1056,11 @@ And with MySQL database:
include::{session-main-resources-dir}org/springframework/session/jdbc/schema-mysql.sql[]
----
==== Transaction management
All JDBC operations in `JdbcOperationsSessionRepository` are executed in a transactional manner.
Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, executing `save` operation in a thread that already participates in a read-only transaction).
[[community]]
== Spring Session Community

View File

@@ -20,6 +20,7 @@ import org.junit.Test;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSessionRepository;
@@ -28,6 +29,7 @@ import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@@ -124,8 +126,12 @@ public class IndexDocTests {
// ... configure JdbcTemplate ...
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
// ... configure transactionManager ...
SessionRepository<? extends ExpiringSession> repository =
new JdbcOperationsSessionRepository(jdbcTemplate);
new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
// end::new-jdbcoperationssessionrepository[]
}

View File

@@ -1,9 +1,10 @@
bootstrapVersion=2.2.2
commonsPoolVersion=2.4.2
jacksonVersion=2.6.5
jspApiVersion=2.0
servletApiVersion=3.0.1
jstlelVersion=1.2.5
version=1.2.0.RC1
version=1.2.0.RC2
springDataRedisVersion=1.6.2.RELEASE
junitVersion=4.12
gebVersion=0.13.1
@@ -14,6 +15,7 @@ springSecurityVersion=4.0.3.RELEASE
springVersion=4.2.5.RELEASE
httpClientVersion=4.5.1
jedisVersion=2.7.3
h2Version=1.4.191
springDataMongoVersion=1.8.2.RELEASE
springShellVersion=1.1.0.RELEASE
springDataGemFireVersion=1.7.4.RELEASE

4
gradle/sample.gradle Normal file
View File

@@ -0,0 +1,4 @@
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -0,0 +1,74 @@
buildscript {
ext {
grailsVersion = project.grailsVersion
}
repositories {
mavenLocal()
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
classpath "org.grails:grails-gradle-plugin:$grailsVersion"
classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.5.0"
classpath "org.grails.plugins:hibernate4:5.0.2"
}
}
apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.grails-gsp"
apply plugin:"asset-pipeline"
ext {
grailsVersion = project.grailsVersion
gradleWrapperVersion = project.gradleWrapperVersion
}
repositories {
mavenLocal()
maven { url "https://repo.grails.org/grails/core" }
}
dependencyManagement {
imports {
mavenBom "org.grails:grails-bom:$grailsVersion"
}
applyMavenExclusions false
}
dependencies {
compile "org.springframework.boot:spring-boot-starter-logging"
compile "org.springframework.boot:spring-boot-autoconfigure"
compile "org.grails:grails-core"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-tomcat"
compile "org.grails:grails-dependencies"
compile "org.grails:grails-web-boot"
compile "org.grails.plugins:cache"
compile "org.grails.plugins:scaffolding"
compile "org.grails.plugins:hibernate4"
compile "org.hibernate:hibernate-ehcache"
console "org.grails:grails-console"
profile "org.grails.profiles:web:3.1.4"
runtime "org.grails.plugins:asset-pipeline"
runtime "com.h2database:h2"
testCompile "org.grails:grails-plugin-testing"
testCompile "org.grails.plugins:geb"
testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
compile "org.springframework.boot:spring-boot-starter-redis"
compile 'org.springframework.session:spring-session:1.1.1.RELEASE'
compile 'org.grails.plugins:spring-security-core:3.0.4'
}
task wrapper(type: Wrapper) {
gradleVersion = gradleWrapperVersion
}
assets {
minifyJs = true
minifyCss = true
}

View File

@@ -0,0 +1,2 @@
grailsVersion=3.1.4
gradleWrapperVersion=2.9

View File

@@ -0,0 +1,28 @@
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.userLookup.userDomainClassName = 'grails3.redis.session.User'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'grails3.redis.session.UserRole'
grails.plugin.springsecurity.authority.className = 'grails3.redis.session.Role'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
[pattern: '/', access: ['permitAll']],
[pattern: '/error', access: ['permitAll']],
[pattern: '/index', access: ['permitAll']],
[pattern: '/index.gsp', access: ['permitAll']],
[pattern: '/shutdown', access: ['permitAll']],
[pattern: '/assets/**', access: ['permitAll']],
[pattern: '/**/js/**', access: ['permitAll']],
[pattern: '/**/css/**', access: ['permitAll']],
[pattern: '/**/images/**', access: ['permitAll']],
[pattern: '/**/favicon.ico', access: ['permitAll']]
]
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/assets/**', filters: 'none'],
[pattern: '/**/js/**', filters: 'none'],
[pattern: '/**/css/**', filters: 'none'],
[pattern: '/**/images/**', filters: 'none'],
[pattern: '/**/favicon.ico', filters: 'none'],
[pattern: '/**', filters: 'JOINED_FILTERS']
]

View File

@@ -0,0 +1,122 @@
---
hibernate:
cache:
queries: false
use_second_level_cache: true
use_query_cache: false
region.factory_class: 'org.hibernate.cache.ehcache.EhCacheRegionFactory'
dataSource:
pooled: true
jmxExport: true
driverClassName: org.h2.Driver
username: sa
password:
environments:
development:
dataSource:
dbCreate: create-drop
url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
test:
dataSource:
dbCreate: update
url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
production:
dataSource:
dbCreate: update
url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
properties:
jmxEnabled: true
initialSize: 5
maxActive: 50
minIdle: 5
maxIdle: 25
maxWait: 10000
maxAge: 600000
timeBetweenEvictionRunsMillis: 5000
minEvictableIdleTimeMillis: 60000
validationQuery: SELECT 1
validationQueryTimeout: 3
validationInterval: 15000
testOnBorrow: true
testWhileIdle: true
testOnReturn: false
jdbcInterceptors: ConnectionState
defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
---
---
grails:
profile: web
codegen:
defaultPackage: grails3.redis.session
spring:
transactionManagement:
proxies: false
info:
app:
name: '@info.app.name@'
version: '@info.app.version@'
grailsVersion: '@info.app.grailsVersion@'
spring:
groovy:
template:
check-template-location: false
---
grails:
mime:
disable:
accept:
header:
userAgents:
- Gecko
- WebKit
- Presto
- Trident
types:
all: '*/*'
atom: application/atom+xml
css: text/css
csv: text/csv
form: application/x-www-form-urlencoded
html:
- text/html
- application/xhtml+xml
js: text/javascript
json:
- application/json
- text/json
multipartForm: multipart/form-data
pdf: application/pdf
rss: application/rss+xml
text: text/plain
hal:
- application/hal+json
- application/hal+xml
xml:
- text/xml
- application/xml
urlmapping:
cache:
maxsize: 1000
controllers:
defaultScope: singleton
converters:
encoding: UTF-8
views:
default:
codec: html
gsp:
encoding: UTF-8
htmlcodec: xml
codecs:
expression: html
scriptlets: html
taglib: none
staticparts: none
endpoints:
jmx:
unique-names: true

View File

@@ -0,0 +1,23 @@
import grails.util.BuildSettings
import grails.util.Environment
// See http://logback.qos.ch/manual/groovy.html for details on configuration
appender('STDOUT', ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}
root(ERROR, ['STDOUT'])
def targetDir = BuildSettings.TARGET_DIR
if (Environment.isDevelopmentMode() && targetDir) {
appender("FULL_STACKTRACE", FileAppender) {
file = "${targetDir}/stacktrace.log"
append = true
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}
logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false)
}

View File

@@ -0,0 +1,3 @@
// Place your Spring DSL code here
beans = {
}

View File

@@ -0,0 +1,8 @@
package grails3.redis.session
import grails.plugin.springsecurity.annotation.Secured
class TestController {
@Secured('ROLE_ADMIN')
def index() { } // Renders `test/index.gsp`
}

View File

@@ -0,0 +1,16 @@
package grails3.redis.session
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}

View File

@@ -0,0 +1,26 @@
package grails3.redis.session
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
private static final long serialVersionUID = 1
String authority
Role(String authority) {
this()
this.authority = authority
}
static constraints = {
authority blank: false, unique: true
}
static mapping = {
cache true
}
}

View File

@@ -0,0 +1,55 @@
package grails3.redis.session
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
transient springSecurityService
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
User(String username, String password) {
this()
this.username = username
this.password = password
}
Set<Role> getAuthorities() {
UserRole.findAllByUser(this)*.role
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
protected void encodePassword() {
password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
}
static transients = ['springSecurityService']
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}

View File

@@ -0,0 +1,103 @@
package grails3.redis.session
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
private static final long serialVersionUID = 1
User user
Role role
UserRole(User u, Role r) {
this()
user = u
role = r
}
@Override
boolean equals(other) {
if (!(other instanceof UserRole)) {
return false
}
other.user?.id == user?.id && other.role?.id == role?.id
}
@Override
int hashCode() {
def builder = new HashCodeBuilder()
if (user) builder.append(user.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
static UserRole get(long userId, long roleId) {
criteriaFor(userId, roleId).get()
}
static boolean exists(long userId, long roleId) {
criteriaFor(userId, roleId).count()
}
private static DetachedCriteria criteriaFor(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}
}
static UserRole create(User user, Role role, boolean flush = false) {
def instance = new UserRole(user: user, role: role)
instance.save(flush: flush, insert: true)
instance
}
static boolean remove(User u, Role r, boolean flush = false) {
if (u == null || r == null) return false
int rowCount = UserRole.where { user == u && role == r }.deleteAll()
if (flush) { UserRole.withSession { it.flush() } }
rowCount
}
static void removeAll(User u, boolean flush = false) {
if (u == null) return
UserRole.where { user == u }.deleteAll()
if (flush) { UserRole.withSession { it.flush() } }
}
static void removeAll(Role r, boolean flush = false) {
if (r == null) return
UserRole.where { role == r }.deleteAll()
if (flush) { UserRole.withSession { it.flush() } }
}
static constraints = {
role validator: { Role r, UserRole ur ->
if (ur.user == null || ur.user.id == null) return
boolean existing = false
UserRole.withNewSession {
existing = UserRole.exists(ur.user.id, r.id)
}
if (existing) {
return 'userRole.exists'
}
}
}
static mapping = {
id composite: ['user', 'role']
version false
}
}

View File

@@ -0,0 +1,56 @@
default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}]
default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL
default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number
default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address
default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}]
default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}]
default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}]
default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}]
default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}]
default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}]
default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation
default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}]
default.blank.message=Property [{0}] of class [{1}] cannot be blank
default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}]
default.null.message=Property [{0}] of class [{1}] cannot be null
default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique
default.paginate.prev=Previous
default.paginate.next=Next
default.boolean.true=True
default.boolean.false=False
default.date.format=yyyy-MM-dd HH:mm:ss z
default.number.format=0
default.created.message={0} {1} created
default.updated.message={0} {1} updated
default.deleted.message={0} {1} deleted
default.not.deleted.message={0} {1} could not be deleted
default.not.found.message={0} not found with id {1}
default.optimistic.locking.failure=Another user has updated this {0} while you were editing
default.home.label=Home
default.list.label={0} List
default.add.label=Add {0}
default.new.label=New {0}
default.create.label=Create {0}
default.show.label=Show {0}
default.edit.label=Edit {0}
default.button.create.label=Create
default.button.edit.label=Edit
default.button.update.label=Update
default.button.delete.label=Delete
default.button.delete.confirm.message=Are you sure?
# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
typeMismatch.java.net.URL=Property {0} must be a valid URL
typeMismatch.java.net.URI=Property {0} must be a valid URI
typeMismatch.java.util.Date=Property {0} must be a valid Date
typeMismatch.java.lang.Double=Property {0} must be a valid number
typeMismatch.java.lang.Integer=Property {0} must be a valid number
typeMismatch.java.lang.Long=Property {0} must be a valid number
typeMismatch.java.lang.Short=Property {0} must be a valid number
typeMismatch.java.math.BigDecimal=Property {0} must be a valid number
typeMismatch.java.math.BigInteger=Property {0} must be a valid number
typeMismatch=Property {0} is type-mismatched

View File

@@ -0,0 +1,24 @@
import grails3.redis.session.*
class BootStrap {
def init = { servletContext ->
def adminRole = new Role('ROLE_ADMIN').save()
def userRole = new Role('ROLE_USER').save()
def testUser = new User('user', 'password').save()
UserRole.create testUser, adminRole
UserRole.withSession {
it.flush()
it.clear()
}
assert User.count() == 1
assert Role.count() == 2
assert UserRole.count() == 1
}
def destroy = {
}
}

View File

@@ -0,0 +1,10 @@
package grails3.redis.session
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application, args)
}
}

View File

@@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<title><g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else></title>
<meta name="layout" content="main">
<g:if env="development"><asset:stylesheet src="errors.css"/></g:if>
</head>
<body>
<g:if env="development">
<g:if test="${Throwable.isInstance(exception)}">
<g:renderException exception="${exception}" />
</g:if>
<g:elseif test="${request.getAttribute('javax.servlet.error.exception')}">
<g:renderException exception="${request.getAttribute('javax.servlet.error.exception')}" />
</g:elseif>
<g:else>
<ul class="errors">
<li>An error has occurred</li>
<li>Exception: ${exception}</li>
<li>Message: ${message}</li>
<li>Path: ${path}</li>
</ul>
</g:else>
</g:if>
<g:else>
<ul class="errors">
<li>An error has occurred</li>
</ul>
</g:else>
</body>
</html>

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>Index</title>
</head>
<body>
Left blank, goto <a href="/test">test</a>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<title>Page Not Found</title>
<meta name="layout" content="main">
<g:if env="development"><asset:stylesheet src="errors.css"/></g:if>
</head>
<body>
<ul class="errors">
<li>Error: Page Not Found (404)</li>
<li>Path: ${request.forwardURI}</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<html>
<head>
<title>Home Page</title>
</head>
<body>
<div id="un">
<sec:loggedInUserInfo field='username'/>
</div>
<div id="session">
${session.id}
</div>
<form action="/logout" method="post">
<input type="submit" value="Log Out"/>
</form>
</body>
</html>

View File

@@ -0,0 +1,98 @@
/*
* 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 grails.test.mixin.integration.Integration
import grails.transaction.Transactional
import org.springframework.boot.test.IntegrationTest
import spock.lang.*
import geb.spock.*
import sample.pages.HomePage
import sample.pages.LoginPage
import sample.pages.IndexPage
import spock.lang.Stepwise
import pages.*
/**
* Functional tests for grails 3 and spring-session
*
* @author Eric Helgeson
*/
@Stepwise
@IntegrationTest("server.port:0")
@Integration(applicationClass=grails3.redis.session.Application)
class HomeSpec extends GebSpec {
def setup() {
}
def cleanup() {
}
void 'Anonymous page not redirected to login'() {
when: 'The index page is visited'
go '/'
then: 'Not redirected'
at IndexPage
}
void 'Unauthenticated user sent to log in page'() {
when: 'The test page is visited'
go '/test/index'
if(title != 'Login') {
println driver.pageSource
}
then: 'The password form is correct'
title == 'Login'
$('#password')
$('#username')
}
void 'Log in views home page'() {
when: 'log in successfully'
to LoginPage
login()
then: 'sent to original page'
at HomePage
and: 'the username is displayed'
username == 'user'
and: 'session id is not blank'
session != ''
and: 'Spring Session Management is being used'
driver.manage().cookies.find { it.name == 'SESSION' }
and: 'Standard Session is NOT being used'
!driver.manage().cookies.find { it.name == 'JSESSIONID' }
}
def 'Log out success'() {
when:
logout()
then:
at IndexPage
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = '/test'
static at = { assert driver.title == 'Home Page'; true}
static content = {
username { $('#un').text() }
session { $('#session').text() }
logout(to:LoginPage) { $('input[type=submit]').click() }
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.pages
import geb.*
/**
* The Index page
*
* @author Eric Helgeson
*/
class IndexPage extends Page {
static url = '/'
static at = { assert driver.title == 'Index'; true}
static content = { }
}

View File

@@ -0,0 +1,38 @@
/*
* 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.pages
import geb.*
/**
* The Login Page
*
* @author Rob Winch
*/
class LoginPage extends Page {
static url = '/login'
static at = { assert driver.title == 'Login'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
login(required:false) { user='user', pass='password' ->
form.username = user
form.password = pass
submit.click()
}
}
}

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session'),

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session'),

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
@@ -23,6 +23,9 @@
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
<!-- end::springSessionRepositoryFilter[] -->

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
@@ -23,6 +23,9 @@
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
<!-- end::springSessionRepositoryFilter[] -->

View File

@@ -0,0 +1 @@
Demonstrates using Spring Session with Spring Boot and Spring Security. You can log in with the username "user" and the password "password".

View File

@@ -0,0 +1,55 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion")
}
}
apply plugin: 'spring-boot'
apply from: JAVA_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
group = 'samples'
dependencies {
compile project(':spring-session-jdbc'),
"org.springframework.boot:spring-boot-starter-jdbc",
"org.springframework.boot:spring-boot-starter-web",
"org.springframework.boot:spring-boot-starter-thymeleaf",
"nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect",
"org.webjars:bootstrap:$bootstrapVersion",
"com.h2database:h2",
"org.springframework.security:spring-security-web:$springSecurityVersion",
"org.springframework.security:spring-security-config:$springSecurityVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
integrationTestCompile gebDependencies,
"org.spockframework:spock-spring:$spockVersion"
}
integrationTest {
doFirst {
def port = reservePort()
def host = 'localhost:' + port
systemProperties['geb.build.baseUrl'] = 'http://'+host+'/'
systemProperties['geb.build.reportsDir'] = 'build/geb-reports'
systemProperties['server.port'] = port
systemProperties['management.port'] = 0
systemProperties['spring.session.redis.namespace'] = project.name
}
}
def reservePort() {
def socket = new ServerSocket(0)
def result = socket.localPort
socket.close()
result
}

View File

@@ -0,0 +1,75 @@
/*
* 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 geb.spock.*
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.IntegrationTest
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import sample.pages.HomePage
import sample.pages.LoginPage
import spock.lang.Stepwise
import pages.*
/**
* Tests the demo that supports multiple sessions
*
* @author Rob Winch
*/
@Stepwise
@ContextConfiguration(classes = Application, loader = SpringApplicationContextLoader)
@WebAppConfiguration
@IntegrationTest
class BootTests extends GebReportingSpec {
def 'Unauthenticated user sent to log in page'() {
when: 'unauthenticated user request protected page'
via HomePage
then: 'sent to the log in page'
at LoginPage
}
def 'Log in views home page'() {
when: 'log in successfully'
login()
then: 'sent to original page'
at HomePage
and: 'the username is displayed'
username == 'user'
and: 'Spring Session Management is being used'
driver.manage().cookies.find { it.name == 'SESSION' }
and: 'Standard Session is NOT being used'
!driver.manage().cookies.find { it.name == 'JSESSIONID' }
}
def 'Log out success'() {
when:
logout()
then:
at LoginPage
}
def 'Logged out user sent to log in page'() {
when: 'logged out user request protected page'
via HomePage
then: 'sent to the log in page'
at LoginPage
}
}

View File

@@ -14,28 +14,20 @@
* limitations under the License.
*/
package sample.config;
package sample.pages
import org.h2.server.web.WebServlet;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import geb.*
/**
* Initializes the H2 {@link WebServlet} so we can access our in memory database from the
* URL "/h2".
* The home page
*
* @author Rob Winch
*/
@Configuration
public class H2Initializer {
@Bean
public ServletRegistrationBean h2Servlet() {
ServletRegistrationBean servletBean = new ServletRegistrationBean();
servletBean.addUrlMappings("/h2/*");
servletBean.setServlet(new WebServlet());
return servletBean;
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Spring Session Sample - Secured Content'; true}
static content = {
username { $('#un').text() }
logout(to:LoginPage) { $('input[type=submit]').click() }
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.pages
import geb.*
/**
* The Links Page
*
* @author Rob Winch
*/
class LoginPage extends Page {
static url = '/login'
static at = { assert driver.title == 'Login Page'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
login(required:false) { user='user', pass='password' ->
form.username = user
form.password = pass
submit.click(HomePage)
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Rob Winch
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.config;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
// tag::class[]
@EnableJdbcHttpSession // <1>
public class HttpSessionConfig {
}
// end::class[]

View File

@@ -0,0 +1,51 @@
/*
* 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.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// @formatter:off
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER");
}
// @formatter:on
// @formatter:off
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring().antMatchers("/h2-console/**");
}
// @formatter:on
}

View File

@@ -0,0 +1,34 @@
/*
* 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.mvc;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Controller for sending the user to the login view.
*
* @author Rob Winch
*
*/
@Controller
public class IndexController {
@RequestMapping("/")
public String index() {
return "index";
}
}

View File

@@ -0,0 +1,4 @@
spring.thymeleaf.cache=false
spring.template.cache=false
spring.datasource.schema=classpath:org/springframework/session/jdbc/schema-h2.sql
spring.h2.console.enabled=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
<head>
<title>Secured Content</title>
</head>
<body>
<div layout:fragment="content">
<h1>Secured Page</h1>
<p>This page is secured using Spring Boot, Spring Session, and Spring Security.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">Spring Session Sample</title>
<link rel="icon" type="image/x-icon" th:href="@{/resources/img/favicon.ico}" href="../static/img/favicon.ico"/>
<link href="/webjars/bootstrap/2.2.2/css/bootstrap.css" rel="stylesheet"></link>
<style type="text/css">
/* Sticky footer styles
-------------------------------------------------- */
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto !important;
height: 100%;
/* Negative indent footer by it's height */
margin: 0 auto -60px;
}
/* Set the fixed height of the footer here */
#push,
#footer {
height: 60px;
}
#footer {
background-color: #f5f5f5;
}
/* Lastly, apply responsive CSS fixes as necessary */
@media (max-width: 767px) {
#footer {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
.container {
width: auto;
max-width: 680px;
}
.container .credit {
margin: 20px 0;
text-align: center;
}
a {
color: green;
}
.navbar-form {
margin-left: 1em;
}
</style>
<link href="/webjars/bootstrap/2.2.2/css/bootstrap-responsive.css" rel="stylesheet"></link>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div id="wrap">
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" th:href="@{/}"><img th:src="@{/resources/img/logo.png}" alt="Spring Security Sample"/></a>
<div class="nav-collapse collapse"
th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}">
<div th:if="${currentUser != null}">
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
<input type="submit" value="Log out" />
</form>
<p id="un" class="navbar-text pull-right" th:text="${currentUser.username}">
sample_user
</p>
</div>
<ul class="nav">
</ul>
</div>
</div>
</div>
</div>
<!-- Begin page content -->
<div class="container">
<div class="alert alert-success"
th:if="${globalMessage}"
th:text="${globalMessage}">
Some Success message
</div>
<div layout:fragment="content">
Fake content
</div>
</div>
<div id="push"><!-- --></div>
</div>
<div id="footer">
<div class="container">
<p class="muted credit">Visit the <a href="http://spring.io/spring-security">Spring Security</a> site for more <a href="https://github.com/spring-projects/spring-security/blob/master/samples/">samples</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -1,15 +1,11 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_6_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-jdbc'),
"org.springframework:spring-web:$springVersion",
"com.h2database:h2:1.4.191",
"com.h2database:h2:$h2Version",
jstlDependencies
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
@@ -23,6 +23,9 @@
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
<!-- end::springSessionRepositoryFilter[] -->
@@ -50,13 +53,13 @@
</servlet-mapping>
<servlet>
<servlet-name>h2console</servlet-name>
<servlet-name>h2Console</servlet-name>
<servlet-class>org.h2.server.web.WebServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>h2console</servlet-name>
<url-pattern>/console/*</url-pattern>
<servlet-name>h2Console</servlet-name>
<url-pattern>/h2-console/*</url-pattern>
</servlet-mapping>
<welcome-file-list>

View File

@@ -1,15 +1,11 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-jdbc'),
"org.springframework:spring-web:$springVersion",
"com.h2database:h2:1.4.191",
"com.h2database:h2:$h2Version",
jstlDependencies
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"

View File

@@ -24,9 +24,8 @@ import org.springframework.web.WebApplicationInitializer;
public class H2ConsoleInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addServlet("h2console", new WebServlet()).addMapping("/console/*");
servletContext.addServlet("h2Console", new WebServlet()).addMapping("/h2-console/*");
}
}

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_6_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -23,6 +23,8 @@
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<!-- end::springSessionRepositoryFilter[] -->

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -21,6 +21,7 @@ dependencies {
"org.springframework.boot:spring-boot-starter-web",
"org.springframework.boot:spring-boot-starter-thymeleaf",
"nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect",
"de.flapdoodle.embed:de.flapdoodle.embed.mongo",
"org.springframework.security:spring-security-web:$springSecurityVersion",
"org.springframework.security:spring-security-config:$springSecurityVersion"
@@ -50,4 +51,4 @@ def reservePort() {
def result = socket.localPort
socket.close()
result
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
class EmbeddedMongoPortLogger implements ApplicationRunner, EnvironmentAware {
private static final Logger logger = LoggerFactory.getLogger(EmbeddedMongoPortLogger.class);
private Environment environment;
public void run(ApplicationArguments args) throws Exception {
String port = this.environment.getProperty("local.mongo.port");
logger.info("Embedded Mongo started on port " + port +
", use 'mongo --port " + port + "' command to connect");
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

View File

@@ -1,2 +1,3 @@
spring.thymeleaf.cache=false
spring.template.cache=false
spring.data.mongodb.port=0

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -1,10 +1,6 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarqube {
skipProject = true
}
apply from: SAMPLE_GRADLE
dependencies {
compile project(':spring-session-data-redis'),

View File

@@ -23,7 +23,7 @@ dependencies {
"org.springframework.boot:spring-boot-starter-thymeleaf",
"org.springframework.boot:spring-boot-starter-websocket",
"org.springframework:spring-websocket:${springVersion}",
"org.springframework.data:spring-data-jpa:1.7.0.RELEASE",
"org.springframework.data:spring-data-jpa",
"nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect",
"com.h2database:h2",
"org.springframework.security:spring-security-web:$springSecurityVersion",

View File

@@ -22,6 +22,7 @@ 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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -51,6 +52,14 @@ 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
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) throws Exception {

View File

@@ -1,2 +1,3 @@
spring.thymeleaf.cache=false
spring.template.cache=false
spring.h2.console.enabled=true

View File

@@ -93,7 +93,7 @@
</div>
<ul class="nav">
<li><a th:href="@{/}">IM</a></li>
<li><a th:href="@{/h2/}">H2</a></li>
<li><a th:href="@{/h2-console/}">H2</a></li>
</ul>
</div>
@@ -121,4 +121,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.Transport;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;
/**
* @author Rob Winch
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
@WebIntegrationTest(randomPort = true)
public class ApplicationTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Value("${local.server.port}")
String port;
@Autowired
WebSocketHandler webSocketHandler;
@Test
public void run() throws Exception {
List<Transport> transports = new ArrayList<Transport>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
ListenableFuture<WebSocketSession> wsSession = sockJsClient.doHandshake(
this.webSocketHandler, "ws://localhost:" + this.port + "/sockjs");
this.thrown.expect(ExecutionException.class);
wsSession.get().sendMessage(new TextMessage("a"));
}
}

View File

@@ -13,6 +13,7 @@ include 'samples:httpsession-gemfire-clientserver-xml'
include 'samples:httpsession-gemfire-p2p'
include 'samples:httpsession-gemfire-p2p-xml'
include 'samples:httpsession-jdbc'
include 'samples:httpsession-jdbc-boot'
include 'samples:httpsession-jdbc-xml'
include 'samples:httpsession-xml'
include 'samples:rest'
@@ -20,6 +21,7 @@ include 'samples:security'
include 'samples:users'
include 'samples:websocket'
include 'samples:mongo'
include 'samples:grails3'
include 'spring-session'
include 'spring-session-data-gemfire'

View File

@@ -28,7 +28,8 @@ dependencies {
integrationTestCompile "redis.clients:jedis:2.4.1",
"org.apache.commons:commons-pool2:2.2",
"com.hazelcast:hazelcast-client:$hazelcastVersion",
"com.h2database:h2:1.4.191"
"com.h2database:h2:$h2Version",
"de.flapdoodle.embed:de.flapdoodle.embed.mongo:1.50.2"
integrationTestRuntime "org.springframework.shell:spring-shell:1.0.0.RELEASE"

View File

@@ -15,13 +15,21 @@
*/
package org.springframework.session.data.mongo;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.mongodb.MongoClient;
import de.flapdoodle.embed.mongo.MongodExecutable;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -30,11 +38,15 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.AbstractITests;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for {@link MongoOperationsSessionRepository} tests.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
*/
abstract public class AbstractMongoRepositoryITests extends AbstractITests {
@@ -364,4 +376,22 @@ abstract public class AbstractMongoRepositoryITests extends AbstractITests {
return this.changedContext.getAuthentication().getName();
}
protected static class BaseConfig {
private int embeddedMongoPort = SocketUtils.findAvailableTcpPort();
@Bean(initMethod = "start", destroyMethod = "stop")
public MongodExecutable embeddedMongoServer() throws IOException {
return MongoITestUtils.embeddedMongoServer(this.embeddedMongoPort);
}
@Bean
@DependsOn("embeddedMongoServer")
public MongoOperations mongoOperations() throws UnknownHostException {
MongoClient mongo = new MongoClient("localhost", this.embeddedMongoPort);
return new MongoTemplate(mongo, "test");
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 org.springframework.session.data.mongo;
import java.io.IOException;
import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.IMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.runtime.Network;
/**
* Utility class for Mongo integration tests.
*
* @author Vedran Pavic
*/
final class MongoITestUtils {
private MongoITestUtils() {
}
/**
* Creates {@link MongodExecutable} for use in integration tests.
* @param port the port for embedded Mongo to bind to
* @return the {@link MongodExecutable} instance
* @throws IOException in case of I/O errors
*/
static MongodExecutable embeddedMongoServer(int port) throws IOException {
IMongodConfig mongodConfig = new MongodConfigBuilder()
.version(Version.Main.PRODUCTION)
.net(new Net(port, Network.localhostIsIPv6()))
.build();
MongodStarter mongodStarter = MongodStarter.getDefaultInstance();
return mongodStarter.prepare(mongodConfig);
}
}

View File

@@ -15,27 +15,27 @@
*/
package org.springframework.session.data.mongo;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import com.fasterxml.jackson.databind.Module;
import com.mongodb.MongoClient;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.geo.GeoModule;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MongoOperationsSessionRepository} that use
* {@link JacksonMongoSessionConverter} based session serialization.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
*/
@ContextConfiguration
public class MongoRepositoryJacksonITests extends AbstractMongoRepositoryITests {
@@ -57,12 +57,7 @@ public class MongoRepositoryJacksonITests extends AbstractMongoRepositoryITests
@Configuration
@EnableMongoHttpSession
static class Config {
@Bean
public MongoOperations mongoOperations() throws UnknownHostException {
return new MongoTemplate(new MongoClient(), "test");
}
static class Config extends BaseConfig {
@Bean
public AbstractMongoSessionConverter mongoSessionConverter() {

View File

@@ -15,23 +15,23 @@
*/
package org.springframework.session.data.mongo;
import java.net.UnknownHostException;
import java.util.Map;
import com.mongodb.MongoClient;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MongoOperationsSessionRepository} that use
* {@link JacksonMongoSessionConverter} based session serialization.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
*/
@ContextConfiguration
public class MongoRepositoryJdkSerializationITests extends AbstractMongoRepositoryITests {
@@ -75,12 +75,7 @@ public class MongoRepositoryJdkSerializationITests extends AbstractMongoReposito
@Configuration
@EnableMongoHttpSession
static class Config {
@Bean
public MongoOperations mongoOperations() throws UnknownHostException {
return new MongoTemplate(new MongoClient(), "test");
}
static class Config extends BaseConfig {
@Bean
public AbstractMongoSessionConverter mongoSessionConverter() {

View File

@@ -37,6 +37,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
@@ -45,6 +46,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@@ -113,6 +115,15 @@ public class JdbcOperationsSessionRepositoryITests {
assertThat(this.repository.getSession(toSave.getId())).isNull();
}
@Test
@Transactional(readOnly = true)
public void savesInReadOnlyTransaction() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
.createSession();
this.repository.save(toSave);
}
@Test
public void putAllOnSingleAttrDoesNotRemoveOld() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
@@ -135,6 +146,26 @@ public class JdbcOperationsSessionRepositoryITests {
this.repository.delete(toSave.getId());
}
@Test
public void updateLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
.createSession();
toSave.setLastAccessedTime(System.currentTimeMillis()
- (MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000));
this.repository.save(toSave);
long lastAccessedTime = System.currentTimeMillis();
toSave.setLastAccessedTime(lastAccessedTime);
this.repository.save(toSave);
ExpiringSession session = this.repository.getSession(toSave.getId());
assertThat(session).isNotNull();
assertThat(session.isExpired()).isFalse();
assertThat(session.getLastAccessedTime()).isEqualTo(lastAccessedTime);
}
@Test
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();

View File

@@ -50,6 +50,13 @@ import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -63,7 +70,14 @@ import org.springframework.util.StringUtils;
* <pre class="code">
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
*
* JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(jdbcTemplate);
* // ... configure jdbcTemplate ...
*
* PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
*
* // ... configure transactionManager ...
*
* JdbcOperationsSessionRepository sessionRepository =
* new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
* </pre>
*
* For additional information on how to create and configure a JdbcTemplate, refer to the
@@ -102,7 +116,7 @@ public class JdbcOperationsSessionRepository implements
private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(SESSION_ID, LAST_ACCESS_TIME, PRINCIPAL_NAME, SESSION_BYTES) VALUES (?, ?, ?, ?)";
private static final String GET_SESSION_QUERY = "SELECT SESSION_BYTES FROM %TABLE_NAME% WHERE SESSION_ID = ?";
private static final String GET_SESSION_QUERY = "SELECT LAST_ACCESS_TIME, SESSION_BYTES FROM %TABLE_NAME% WHERE SESSION_ID = ?";
private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ?, PRINCIPAL_NAME = ?, SESSION_BYTES = ? WHERE SESSION_ID = ?";
@@ -110,7 +124,7 @@ public class JdbcOperationsSessionRepository implements
private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% WHERE SESSION_ID = ?";
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT SESSION_BYTES FROM %TABLE_NAME% WHERE PRINCIPAL_NAME = ?";
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT LAST_ACCESS_TIME, SESSION_BYTES FROM %TABLE_NAME% WHERE PRINCIPAL_NAME = ?";
private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY = "DELETE FROM %TABLE_NAME% WHERE LAST_ACCESS_TIME < ?";
@@ -121,6 +135,8 @@ public class JdbcOperationsSessionRepository implements
private final JdbcOperations jdbcOperations;
private final TransactionOperations transactionOperations;
private final RowMapper<ExpiringSession> mapper = new ExpiringSessionMapper();
/**
@@ -140,22 +156,26 @@ public class JdbcOperationsSessionRepository implements
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* default ${JdbcOperations} to manage sessions.
* default {@link JdbcOperations} to manage sessions.
* @param dataSource the {@link DataSource} to use
* @param transactionManager the {@link PlatformTransactionManager} to use
*/
public JdbcOperationsSessionRepository(DataSource dataSource) {
this(createDefaultTemplate(dataSource));
public JdbcOperationsSessionRepository(DataSource dataSource,
PlatformTransactionManager transactionManager) {
this(createDefaultJdbcTemplate(dataSource), transactionManager);
}
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided ${JdbcOperations} to manage sessions.
* provided {@link JdbcOperations} to manage sessions.
* @param jdbcOperations the {@link JdbcOperations} to use
* @param transactionManager the {@link PlatformTransactionManager} to use
*/
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
PlatformTransactionManager transactionManager) {
Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
this.jdbcOperations = jdbcOperations;
this.transactionOperations = createTransactionTemplate(transactionManager);
this.conversionService = createDefaultConversionService();
}
@@ -185,7 +205,6 @@ public class JdbcOperationsSessionRepository implements
/**
* Sets the {@link ConversionService} to use.
*
* @param conversionService the converter to set
*/
public void setConversionService(ConversionService conversionService) {
@@ -203,49 +222,65 @@ public class JdbcOperationsSessionRepository implements
public void save(final JdbcSession session) {
if (session.isNew()) {
this.jdbcOperations.update(getQuery(CREATE_SESSION_QUERY),
new PreparedStatementSetter() {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime());
ps.setString(3, session.getPrincipalName());
JdbcOperationsSessionRepository.this.lobHandler
.getLobCreator()
.setBlobAsBytes(ps, 4, serialize(session.delegate));
}
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(CREATE_SESSION_QUERY),
new PreparedStatementSetter() {
});
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime());
ps.setString(3, session.getPrincipalName());
serialize(ps, 4, session.delegate);
}
});
}
});
}
else {
if (session.isAttributesChanged()) {
this.jdbcOperations.update(getQuery(UPDATE_SESSION_QUERY),
new PreparedStatementSetter() {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
public void setValues(PreparedStatement ps)
throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getPrincipalName());
JdbcOperationsSessionRepository.this.lobHandler
.getLobCreator().setBlobAsBytes(ps, 3,
serialize(session.delegate));
ps.setString(4, session.getId());
}
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(UPDATE_SESSION_QUERY),
new PreparedStatementSetter() {
});
public void setValues(PreparedStatement ps)
throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getPrincipalName());
serialize(ps, 3, session.delegate);
ps.setString(4, session.getId());
}
});
}
});
}
else if (session.isLastAccessTimeChanged()) {
this.jdbcOperations.update(
getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY),
new PreparedStatementSetter() {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
public void setValues(PreparedStatement ps)
throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getId());
}
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY),
new PreparedStatementSetter() {
});
public void setValues(PreparedStatement ps)
throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getId());
}
});
}
});
}
else {
return;
@@ -254,14 +289,22 @@ public class JdbcOperationsSessionRepository implements
session.clearChangeFlags();
}
public JdbcSession getSession(String id) {
ExpiringSession session = null;
try {
session = this.jdbcOperations.queryForObject(getQuery(GET_SESSION_QUERY),
new Object[] { id }, this.mapper);
}
catch (EmptyResultDataAccessException ignored) {
}
public JdbcSession getSession(final String id) {
ExpiringSession session = this.transactionOperations.execute(new TransactionCallback<ExpiringSession>() {
public ExpiringSession doInTransaction(TransactionStatus status) {
try {
return JdbcOperationsSessionRepository.this.jdbcOperations.queryForObject(
getQuery(GET_SESSION_QUERY),
new Object[] { id },
JdbcOperationsSessionRepository.this.mapper);
}
catch (EmptyResultDataAccessException ignored) {
return null;
}
}
});
if (session != null) {
if (session.isExpired()) {
@@ -274,19 +317,33 @@ public class JdbcOperationsSessionRepository implements
return null;
}
public void delete(String id) {
this.jdbcOperations.update(getQuery(DELETE_SESSION_QUERY), id);
public void delete(final String id) {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(DELETE_SESSION_QUERY), id);
}
});
}
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName,
String indexValue) {
final String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<ExpiringSession> sessions = this.jdbcOperations.query(
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY),
new Object[] { indexValue }, this.mapper);
List<ExpiringSession> sessions = this.transactionOperations.execute(new TransactionCallback<List<ExpiringSession>>() {
public List<ExpiringSession> doInTransaction(TransactionStatus status) {
return JdbcOperationsSessionRepository.this.jdbcOperations.query(
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY),
new Object[] { indexValue },
JdbcOperationsSessionRepository.this.mapper);
}
});
Map<String, JdbcSession> sessionMap = new HashMap<String, JdbcSession>(
sessions.size());
@@ -305,36 +362,42 @@ public class JdbcOperationsSessionRepository implements
? this.defaultMaxInactiveInterval
: MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
long sessionsValidFromTime = now - (maxInactiveIntervalSeconds * 1000);
final long sessionsValidFromTime = now - (maxInactiveIntervalSeconds * 1000);
if (logger.isDebugEnabled()) {
logger.debug(
"Cleaning up sessions older than " + new Date(sessionsValidFromTime));
}
int deletedCount = this.jdbcOperations.update(
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY),
sessionsValidFromTime);
int deletedCount = this.transactionOperations.execute(new TransactionCallback<Integer>() {
public Integer doInTransaction(TransactionStatus transactionStatus) {
return JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY),
sessionsValidFromTime);
}
});
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
private static JdbcTemplate createDefaultTemplate(DataSource dataSource) {
private static JdbcTemplate createDefaultJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.afterPropertiesSet();
return jdbcTemplate;
}
protected String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private byte[] serialize(ExpiringSession session) {
return (byte[]) this.conversionService.convert(session,
TypeDescriptor.valueOf(ExpiringSession.class),
TypeDescriptor.valueOf(byte[].class));
private static TransactionTemplate createTransactionTemplate(
PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(
transactionManager);
transactionTemplate.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.afterPropertiesSet();
return transactionTemplate;
}
private static GenericConversionService createDefaultConversionService() {
@@ -346,6 +409,26 @@ public class JdbcOperationsSessionRepository implements
return converter;
}
protected String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private void serialize(PreparedStatement ps, int paramIndex, ExpiringSession session)
throws SQLException {
this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex,
(byte[]) this.conversionService.convert(session,
TypeDescriptor.valueOf(ExpiringSession.class),
TypeDescriptor.valueOf(byte[].class)));
}
private ExpiringSession deserialize(ResultSet rs, String columnName)
throws SQLException {
return (ExpiringSession) this.conversionService.convert(
this.lobHandler.getBlobAsBytes(rs, columnName),
TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(ExpiringSession.class));
}
/**
* The {@link ExpiringSession} to use for {@link JdbcOperationsSessionRepository}.
*
@@ -473,12 +556,9 @@ public class JdbcOperationsSessionRepository implements
private class ExpiringSessionMapper implements RowMapper<ExpiringSession> {
public ExpiringSession mapRow(ResultSet rs, int rowNum) throws SQLException {
return (ExpiringSession) JdbcOperationsSessionRepository.this.conversionService
.convert(
JdbcOperationsSessionRepository.this.lobHandler
.getBlobAsBytes(rs, "SESSION_BYTES"),
TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(ExpiringSession.class));
ExpiringSession session = deserialize(rs, "SESSION_BYTES");
session.setLastAccessedTime(rs.getLong("LAST_ACCESS_TIME"));
return session;
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.StringUtils;
/**
@@ -71,9 +72,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
@Bean
public JdbcOperationsSessionRepository sessionRepository(
@Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations) {
JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(
jdbcOperations);
@Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations,
PlatformTransactionManager transactionManager) {
JdbcOperationsSessionRepository sessionRepository =
new JdbcOperationsSessionRepository(jdbcOperations, transactionManager);
String tableName = getTableName();
if (StringUtils.hasText(tableName)) {
sessionRepository.setTableName(tableName);

View File

@@ -228,7 +228,7 @@ public class SessionRepositoryFilter<S extends ExpiringSession>
}
/**
* Uses the HttpSessionStrategy to write the session id tot he response and
* Uses the HttpSessionStrategy to write the session id to the response and
* persist the Session.
*/
private void commitSession() {

View File

@@ -26,6 +26,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@@ -38,6 +39,8 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.AdditionalMatchers.and;
@@ -48,6 +51,7 @@ import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Matchers.startsWith;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -72,17 +76,21 @@ public class JdbcOperationsSessionRepositoryTests {
@Mock
private JdbcOperations jdbcOperations;
@Mock
private PlatformTransactionManager transactionManager;
private JdbcOperationsSessionRepository repository;
@Before
public void setUp() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
this.repository = new JdbcOperationsSessionRepository(
this.jdbcOperations, this.transactionManager);
}
@Test
public void constructorDataSource() {
JdbcOperationsSessionRepository repository = new JdbcOperationsSessionRepository(
this.dataSource);
this.dataSource, this.transactionManager);
assertThat(ReflectionTestUtils.getField(repository, "jdbcOperations"))
.isNotNull();
@@ -93,7 +101,7 @@ public class JdbcOperationsSessionRepositoryTests {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Property 'dataSource' is required");
new JdbcOperationsSessionRepository((DataSource) null);
new JdbcOperationsSessionRepository((DataSource) null, this.transactionManager);
}
@Test
@@ -101,7 +109,15 @@ public class JdbcOperationsSessionRepositoryTests {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("JdbcOperations must not be null");
new JdbcOperationsSessionRepository((JdbcOperations) null);
new JdbcOperationsSessionRepository((JdbcOperations) null, this.transactionManager);
}
@Test
public void constructorNullTransactionManager() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Property 'transactionManager' is required");
new JdbcOperationsSessionRepository(this.jdbcOperations, null);
}
@Test
@@ -168,6 +184,7 @@ public class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"),
isA(PreparedStatementSetter.class));
}
@@ -181,6 +198,7 @@ public class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(
and(startsWith("UPDATE"), contains("SESSION_BYTES")),
isA(PreparedStatementSetter.class));
@@ -195,6 +213,7 @@ public class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(
and(startsWith("UPDATE"), not(contains("SESSION_BYTES"))),
isA(PreparedStatementSetter.class));
@@ -212,7 +231,6 @@ public class JdbcOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
public void getSessionNotFound() {
String sessionId = "testSessionId";
@@ -220,15 +238,16 @@ public class JdbcOperationsSessionRepositoryTests {
.getSession(sessionId);
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"),
eq(new Object[] { sessionId }), isA(RowMapper.class));
}
@Test
@SuppressWarnings("unchecked")
public void getSessionExpired() {
MapSession expired = new MapSession();
expired.setMaxInactiveIntervalInSeconds(0);
expired.setLastAccessedTime(System.currentTimeMillis() -
(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000));
given(this.jdbcOperations.queryForObject(startsWith("SELECT"),
eq(new Object[] { expired.getId() }), isA(RowMapper.class)))
.willReturn(expired);
@@ -237,6 +256,7 @@ public class JdbcOperationsSessionRepositoryTests {
.getSession(expired.getId());
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"),
eq(new Object[] { expired.getId() }), isA(RowMapper.class));
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"),
@@ -244,7 +264,6 @@ public class JdbcOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
public void getSessionFound() {
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
@@ -258,6 +277,7 @@ public class JdbcOperationsSessionRepositoryTests {
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.isNew()).isFalse();
assertThat(session.getAttribute("savedName")).isEqualTo("savedValue");
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"),
eq(new Object[] { saved.getId() }), isA(RowMapper.class));
}
@@ -268,6 +288,7 @@ public class JdbcOperationsSessionRepositoryTests {
this.repository.delete(sessionId);
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId));
}
@@ -283,7 +304,6 @@ public class JdbcOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
public void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
String principal = "username";
@@ -293,12 +313,12 @@ public class JdbcOperationsSessionRepositoryTests {
principal);
assertThat(sessions).isEmpty();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"),
eq(new Object[] { principal }), isA(RowMapper.class));
}
@Test
@SuppressWarnings("unchecked")
public void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal,
@@ -319,6 +339,7 @@ public class JdbcOperationsSessionRepositoryTests {
principal);
assertThat(sessions).hasSize(2);
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"),
eq(new Object[] { principal }), isA(RowMapper.class));
}
@@ -327,7 +348,16 @@ public class JdbcOperationsSessionRepositoryTests {
public void cleanupExpiredSessions() {
this.repository.cleanUpExpiredSessions();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong());
}
private void assertPropagationRequiresNew() {
ArgumentCaptor<TransactionDefinition> argument =
ArgumentCaptor.forClass(TransactionDefinition.class);
verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture());
assertThat(argument.getValue().getPropagationBehavior())
.isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
}

View File

@@ -28,10 +28,10 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -133,7 +133,7 @@ public class JdbcHttpSessionConfigurationTests {
@Test
public void customConversionServiceConfiguration() {
registerAndRefresh(CustomDeserializingConverterConfiguration.class);
registerAndRefresh(CustomConversionServiceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context
.getBean(JdbcOperationsSessionRepository.class);
@@ -163,6 +163,11 @@ public class JdbcHttpSessionConfigurationTests {
return mock(DataSource.class);
}
@Bean
public PlatformTransactionManager transactionManager() {
return mock(PlatformTransactionManager.class);
}
}
@Configuration
@@ -194,19 +199,7 @@ public class JdbcHttpSessionConfigurationTests {
@Configuration
@EnableJdbcHttpSession
static class CustomSerializingConverterConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
public Converter<Object, byte[]> springSessionSerializingConverter() {
return mock(Converter.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class CustomDeserializingConverterConfiguration extends BaseConfiguration {
static class CustomConversionServiceConfiguration extends BaseConfiguration {
@Bean
public ConversionService springSessionConversionService() {