Start Documentation

Issue gh-41
This commit is contained in:
Rob Winch
2014-12-23 17:26:27 -06:00
parent 59dbd75ca2
commit 64a0312bf7
27 changed files with 1653 additions and 84 deletions

View File

@@ -183,7 +183,7 @@ public class Initializer extends AbstractHttpSessionApplicationInitializer {
= Sample
The code contains a https://github.com/spring-projects/spring-session/tree/master/samples/web[sample web application]. To run the sample:
The code contains a https://github.com/spring-projects/spring-session/tree/master/samples/httpsession[sample web application]. To run the sample:
. Obtain the source by https://github.com/spring-projects/spring-session[cloning the repository] or https://github.com/spring-projects/spring-session/archive/master.zip[downloading] it.
. Run the application using gradle

View File

@@ -7,8 +7,7 @@ buildscript {
classpath("org.springframework.build.gradle:propdeps-plugin:0.0.7")
classpath("org.springframework.build.gradle:spring-io-plugin:0.0.3.RELEASE")
classpath('me.champeau.gradle:gradle-javadoc-hotfix-plugin:0.1')
classpath('org.asciidoctor:asciidoctor-gradle-plugin:0.7.0')
classpath('org.asciidoctor:asciidoctor-java-integration:0.1.4.preview.1')
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.2'
}
}
@@ -19,7 +18,12 @@ ext.JAVA_GRADLE = "$rootDir/gradle/java.gradle"
ext.MAVEN_GRADLE = "$rootDir/gradle/publish-maven.gradle"
ext.TOMCAT_GRADLE = "$rootDir/gradle/tomcat.gradle"
ext.releaseBuild = version.endsWith('RELEASE')
ext.snapshotBuild = version.endsWith('SNAPSHOT')
ext.milestoneBuild = !(releaseBuild || snapshotBuild)
apply plugin: 'sonar-runner'
apply plugin: 'base'
sonarRunner {
@@ -34,4 +38,23 @@ sonarRunner {
property "sonar.links.scm_dev", 'https://github.com/spring-projects/spring-session.git'
property "sonar.java.coveragePlugin", "jacoco"
}
}
task configDocsZip(dependsOn: ':docs:asciidoctor') << {
project.tasks.docsZip.from(project(':docs').asciidoctor) {
into('docs')
}
}
task docsZip(type: Zip, dependsOn: 'configDocsZip') {
group = "Distribution"
baseName = "spring-session"
classifier = "docs"
description = "Builds -${classifier} archive containing api and reference " +
"for deployment."
}
artifacts {
archives docsZip
}

42
docs/build.gradle Normal file
View File

@@ -0,0 +1,42 @@
apply plugin: 'java'
apply plugin: 'org.asciidoctor.convert'
asciidoctorj {
}
dependencies {
testCompile project(':spring-session'),
"org.springframework.data:spring-data-redis:$springDataRedisVersion",
"org.springframework:spring-websocket:${springVersion}",
"org.springframework:spring-messaging:${springVersion}",
'junit:junit:4.11',
'org.mockito:mockito-core:1.9.5',
"org.springframework:spring-test:$springVersion",
'org.easytesting:fest-assert:1.4',
"redis.clients:jedis:2.4.1"
}
asciidoctor {
def ghTag = snapshotBuild ? 'master' : project.version
def ghUrl = "https://github.com/spring-projects/spring-session/tree/$ghTag/"
attributes 'version-snapshot': snapshotBuild,
'version-milestone': milestoneBuild,
'version-release': releaseBuild,
'gh-url': ghUrl,
'gh-samples-url': "$ghUrl/samples/",
'download-url' : "https://github.com/spring-projects/spring-session/archive/${ghTag}.zip",
'spring-session-version' : version,
'spring-version' : springVersion,
'docs-test-dir' : rootProject.projectDir.path + '/docs/src/test/java/',
'samples-dir' : rootProject.projectDir.path + '/samples/',
'source-highlighter' : 'coderay',
'imagesdir':'./images',
'icons': 'font',
'sectanchors':'',
'idprefix':'',
'idseparator':'-',
'docinfo1':'true',
'revnumber' : project.version
}

View File

@@ -0,0 +1,134 @@
= Spring Session - Spring Boot
Rob Winch
:toc:
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Spring Boot.
NOTE: The completed guide can be found in the <<boot-sample, 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-data-redis</artifactId>
<version>{spring-session-version}</version>
<type>pom<type>
</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::[]
[[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}boot/src/main/java/sample/config/HttpSessionConfig.java[]
----
<1> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
In this instance Spring Session is backed by Redis.
<2> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
In our example, we are connecting to localhost on the default port (6379).
For more information on configuring Spring Data Redis, refer to the http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/[reference documentation].
== Servlet Container Initialization
Our <<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.
[[boot-sample]]
== boot Sample Application
The boot Sample Application demonstrates how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Spring Boot.
=== Running the boot Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
$ ./gradlew :samples:boot:tomcatRun
You should now be able to access the application at http://localhost:8080/
=== 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.
=== 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/ and observe that we are no longer authenticated.

View File

@@ -0,0 +1,160 @@
= Spring Session - HttpSession (Quick Start)
Rob Winch
:toc:
This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession`.
NOTE: The completed guide can be found in the <<httpsession-sample, httpsession sample application>>.
// tag::config[]
== Updating Dependencies
Before you use Spring Session, you must ensure to update your dependencies.
If you are using Maven, ensure to add the following dependencies:
.pom.xml
[source,xml]
[subs="verbatim,attributes"]
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>{spring-session-version}</version>
<type>pom<type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>{spring-version}</version>
</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::[]
[[httpsession-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/src/main/java/sample/Config.java[]
----
<1> We import an embedded Redis Server so that there is no need to start up Redis external of our application.
In a production application this is not necessary since we would point our connection to an external Redis instance.
<2> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
In this instance Spring Session is backed by Redis.
<3> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
In our example, we are connecting to localhost on the default port (6379).
For more information on configuring Spring Data Redis, refer to the http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/[reference documentation].
== Servlet Container Initialization
Our <<httpsession-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` both of these steps extremely easy.
You can find an example below:
.src/main/java/sample/Initializer.java
[source,java]
----
include::{samples-dir}httpsession/src/main/java/sample/Initializer.java[]
----
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
<1> The first step is to extend `AbstractHttpSessionApplicationInitializer`.
This ensures that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request.
<2> `AbstractHttpSessionApplicationInitializer` also provides a mechanism to easily ensure Spring loads our `Config`.
// end::config[]
[[httpsession-sample]]
== httpsession Sample Application
=== Running the httpsession Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
$ ./gradlew :samples:httpsession:tomcatRun
You should now be able to access the application at http://localhost:8080/
=== Exploring the httpsession Sample Application
Try using the application. Fill out the form with the following information:
* **Attribute Name** *username*
* **Attribute Value** *rob*
Now click the **Set Attribute** button. You should now see the values displayed in the table.
=== How does it work?
We interact with the standard `HttpSession` in the `SessionServlet` shown below:
.src/main/java/sample/SessionServlet.java
[source,java]
----
include::{samples-dir}httpsession/src/main/java/sample/SessionServlet.java[]
----
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis.
Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://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/ and observe that the attribute we added is no longer displayed.

View File

@@ -0,0 +1,162 @@
= Spring Session and Spring Security
Rob Winch
:toc:
This guide describes how to use Spring Session along with Spring Security.
It assumes you have already applied Spring Security to your application.
NOTE: The completed guide can be found in the <<security-sample, security sample application>>.
== Updating Dependencies
Before you use Spring Session, you must ensure to update your dependencies.
If you are using Maven, ensure to add the following dependencies:
.pom.xml
[source,xml]
[subs="verbatim,attributes"]
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>{spring-session-version}</version>
<type>pom<type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>{spring-version}</version>
</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::[]
[[security-spring-configuration]]
== Spring Configuration
After adding the required dependencies, we can create our Spring configuration.
The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session.
Add the following Spring Configuration:
[source,java]
----
include::{samples-dir}security/src/main/java/sample/Config.java[]
----
<1> We import an embedded Redis Server so that there is no need to start up Redis external of our application.
In a production application this is not necessary since we would point our connection to an external Redis instance.
<2> The `@EnableRedisHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter.
The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session.
In this instance Spring Session is backed by Redis.
<3> We create a `RedisConnectionFactory` that connects Spring Session to the Redis Server.
In our example, we are connecting to localhost on the default port (6379).
For more information on configuring Spring Data Redis, refer to the http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/[reference documentation].
== Servlet Container Initialization
Our <<security-spring-configuration,Spring Configuration>> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`.
The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session.
In order for our `Filter` to do its magic, Spring needs to load our `Config` class.
Since our application is already loading Spring configuration using our `SecurityInitializer` class, we can simply add our Config class to it.
.src/main/java/sample/SecurityInitializer.java
[source,java]
----
include::{samples-dir}security/src/main/java/sample/SecurityInitializer.java[]
----
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request.
It is extremely important that Spring Session's `springSessionRepositoryFilter` is invoked before Spring Security's `springSecurityFilterChain`.
This ensures that the `HttpSession` that Spring Security uses is backed by Spring Session.
Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this extremely easy.
You can find an example below:
.src/main/java/sample/Initializer.java
[source,java]
----
include::{samples-dir}security/src/main/java/sample/Initializer.java[]
----
NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`.
By extending `AbstractHttpSessionApplicationInitializer` we ensure that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request before Spring Security's `springSecurityFilterChain` .
[[security-sample]]
== security Sample Application
=== Running the security Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
$ ./gradlew :samples:security:tomcatRun
You should now be able to access the application at http://localhost:8080/
=== 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.
=== 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/ and observe that we are no longer authenticated.

View File

@@ -0,0 +1,153 @@
= Spring Session - Multiple Sessions
Rob Winch
:toc:
This guide describes how to use Spring Session to manage multiple simultaneous browser sessions (i.e Google Accounts).
== Integrating with Spring Session
The steps to integrate with Spring Session are exactly the same as those outline in the link:httpsession.html[HttpSession Guide], so we will skip to running the sample application.
[[users-sample]]
== users Sample Application
The users application demonstrates how to allow an application to manage multiple simultaneous browser sessions (i.e. Google Accounts).
=== Running the users Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
$ ./gradlew :samples:users:tomcatRun
You should now be able to access the application at http://localhost:8080/
=== Exploring the users Sample Application
Try using the application. Authenticate with the following information:
* **Username** _rob_
* **Password** _rob_
Now click the **Login** button. You should now be authenticated as the user **rob**.
We can click on links and our user information is preserved.
* Click on the **Link** link in the navigation bar at the top
* Observe we are still authenticated as **rob**
Let's add an another account.
* Return to the *Home* page
* Click on the arrow next to *rob* in the upper right hand corner
* Click **Add Account**
The log in form is displayed again. Authenticate with the following information:
* **Username** _luke_
* **Password** _luke_
Now click the **Login** button. You should now be authenticated as the user **luke**.
We can click on links and our user information is preserved.
* Click on the **Link** link in the navigation bar at the top
* Observe we are still authenticated as **luke**
Where did our original user go? Let's switch to our original account.
* Click on the arrow next to *luke* in the upper right hand corner.
* Click on **Switch Account** -> *rob*
We are now using the session associated with *rob*.
== How does it work?
// tag::how-does-it-work[]
Let's take a look at how Spring Session keeps track of multiple sessions.
=== Managing a Single Session
Spring Session keeps track of the `HttpSession` by adding a value to a cookie named SESSION.
For example, the SESSION cookie might have a value of:
7e8383a4-082c-4ffe-a4bc-c40fd3363c5e
=== Adding a Session
We can add another session by requesting a URL that contains a special parameter in it.
By default the parameter name is *_s*. For example, the following URL would create a new session:
http://localhost:8080/?_s=1
NOTE: The parameter value does not indicate the actual session id.
This is important because we never want to allow the session id to be determined by a client to avoid https://www.owasp.org/index.php/Session_fixation[session fixation attacks].
Additionally, we do not want the session id to be leaked since it is sent as a query parameter.
Remember sensitive information should only be transmitted as a header or in the body of the request.
Rather than creating the URL ourselves, we can utilize the `HttpSessionManager` to do this for us.
We can obtain the `HttpSessionManager` from the `HttpServletRequest` using the following:
.src/main/java/sample/UserAccountsFilter.java
[source,java,indent=0]
----
include::{samples-dir}users/src/main/java/sample/UserAccountsFilter.java[tags=HttpSessionManager]
----
We can now use it to create a URL to add another session.
.src/main/java/sample/UserAccountsFilter.java
[source,java,indent=0]
----
include::{samples-dir}users/src/main/java/sample/UserAccountsFilter.java[tags=addAccountUrl]
----
<1> We have an existing variable named `unauthenticatedAlias`.
The value is an alias that points to existing unauthenticated session.
If no such session exists, the value is null.
This ensures if we have an existing unauthenticated session that we use it instead of creating a new session.
<2> If all of our sessions are already associated to a user, we create a new session alias.
<3> If a there exists a session that is not associated to a user, we use its session alias.
<4> Finally, we create the add account URL.
The URL contains a session alias that either points to an existing unauthenticated session or is an alias that is unused thus signaling to create a new session associated to that alias.
Now our SESSION cookie looks something like this:
0 7e8383a4-082c-4ffe-a4bc-c40fd3363c5e 1 1d526d4a-c462-45a4-93d9-84a39b6d44ad
Such that:
* There is a session with the id *7e8383a4-082c-4ffe-a4bc-c40fd3363c5e*
** The alias for this session is *0*.
For example, if the URL is http://localhost:8080/?_s=0 this alias would be used.
** This is the default session.
This means that if no session alias is specified, then this session is used.
For example, if the URL is http://localhost:8080/ this session would be used.
* There is a session with the id *1d526d4a-c462-45a4-93d9-84a39b6d44ad*
** The alias for this session is *1*.
If the session alias is *1*, then this session is used.
For example, if the URL is http://localhost:8080/?_s=1 this alias would be used.
=== Automatic Session Alias Inclusion with encodeURL
The nice thing about specifying the session alias in the URL is that we can have multiple tabs open with different active sessions.
The bad thing is that we need to include the session alias in every URL of our application.
Fortunately, Spring Session will automatically include the session alias in any URL that passes through http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletResponse.html#encodeURL(java.lang.String)[HttpServletResponse#encodeURL(java.lang.String)]
This means that if you are using standard tag libraries the session alias is automatically included in the URL.
For example, if we are currently using the session with the alias of *1*, then the following:
.src/main/webapp/index.jsp
[source,xml,indent=0]
----
include::{samples-dir}users/src/main/webapp/index.jsp[tags=link]
----
will output a link of:
[source,html]
----
<a id="navLink" href="/link.jsp?_s=1">Link</a>
----
end::how-does-it-work[]

View File

@@ -0,0 +1,130 @@
= Spring Session - WebSocket
Rob Winch
:toc:
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
This guide describes how to use Spring Session to ensure that WebSocket messages keep your HttpSession alive.
// tag::disclaimer[]
NOTE: Spring Session's WebSocket support only works with Spring's WebSocket support.
Specifically it does not work with using https://www.jcp.org/en/jsr/detail?id=356[JSR-356] directly.
This is due to the fact that JSR-356 does not have a mechanism for intercepting incoming WebSocket messages.
// end::disclaimer[]
== HttpSession Setup
The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the link:httpsession.html[HttpSession Guide].
Please make sure you have already integrated Spring Session with the HttpSession before proceeding.
// tag::config[]
[[websocket-spring-configuration]]
== Spring Configuration
In a typical Spring WebSocket application users would extend `AbstractWebSocketMessageBrokerConfigurer`.
For example, the configuration might look something like the following:
[source,java]
----
include::{websocketdoc-test-dir}WebSocketConfig.java[tags=class]
----
To hook in the Spring Session support, we need to ensure
* `WebSocketConnectHandlerDecoratorFactory` is added as a `WebSocketHandlerDecoratorFactory` to `WebSocketTransportRegistration`.
This ensures a custom `SessionConnectEvent` is fired that contains the `WebSocketSession`.
The `WebSocketSession` is necessary to terminate any WebSocket connections that are still open when a Spring Session is terminated.
* `SessionRepositoryMessageInterceptor` is added as a `HandshakeInterceptor` to every `StompWebSocketEndpointRegistration`.
This ensures that the Session is added to the WebSocket properties to enable updating the last accessed time.
* `SessionRepositoryMessageInterceptor` is added as a `ChannelInterceptor` to our inbound `ChannelRegistration`.
This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.
* `WebSocketRegistryListener` is created as a Spring Bean.
This ensures that we have a mapping of all of the Session id to the corresponding WebSocket connections.
By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is terminated.
This is quite a bit of work to get things working.
Fortunately, Spring Session provides a convenience class named `AbstractSessionWebSocketMessageBrokerConfigurer` that hides the complexity of the configuration.
An example is provided below:
.src/main/java/samples/config/WebSocketConfig.java
[source,java]
----
include::{samples-dir}websocket/src/main/java/sample/config/WebSocketConfig.java[tags=class]
----
We only need to change two things to use Spring Session:
<1> Instead of extending `AbstractWebSocketMessageBrokerConfigurer` we extend `AbstractSessionWebSocketMessageBrokerConfigurer`
<2> We rename the `registerStompEndpoints` method to `configureStompEndpoints`
// end::config[]
[[websocket-sample]]
== websocket Sample Application
The websocket sample application demonstrates how to use Spring Session with WebSockets.
=== Running the websocket Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
[TIP]
====
For the purposes of testing session expiration, you may want to change the session expiration to be 1 minute (default is 30 minutes) by removing the comment from the following file before starting the application:
.src/main/java/samples/config/WebSecurityConfig.java
[source,java]
----
include::{samples-dir}websocket/src/main/java/sample/config/WebSecurityConfig.java[tags=enable-redis-httpsession]
----
====
$ ./gradlew :samples:websocket:bootRun
You should now be able to access the application at http://localhost:8080/
=== Exploring the websocket Sample Application
Try using the application. Authenticate with the following information:
* **Username** _rob_
* **Password** _password_
Now click the **Login** button. You should now be authenticated as the user **rob**.
Open an incognito window and access http://localhost:8080/
You will be prompted with a log in form. Authenticate with the following information:
* **Username** _luke_
* **Password** _password_
Now send a message from *rob* to *luke*. The message should appear.
Wait for two minutes and try sending a message from *rob* to *luke* again.
You will see that the message is no longer sent.
[NOTE]
.Why two minutes?
====
Spring Session will expire in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds.
To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions.
This means you will need to wait at most two minutes before the WebSocket connection is terminated.
====
Try accessing http://localhost:8080/
You will be prompted to authenticate again.
This demonstrates that the session properly expires.
Now repeat the same exercise, but instead of waiting two minutes send a message from *each* of the users every 30 seconds.
You will see that the messages continue to be sent.
Try accessing http://localhost:8080/
You will not be prompted to authenticate again.
This demonstrates the session is kept alive.
NOTE: Only messages sent from a user keep the session alive.
This is because only messages coming from a user imply user activity.
Messages received do not imply activity and thus do not renew the session expiration.

View File

@@ -0,0 +1,366 @@
= Spring Session
Rob Winch
:doctype: book
:indexdoc-tests: {docs-test-dir}docs/IndexDocTests.java
:websocketdoc-test-dir: {docs-test-dir}docs/websocket/
:toc: left
[[abstract]]
Spring Session provides an API and implementations for managing a user's session information.
It also provides transparent integration with:
* HttpSession - allows for simple clustered sessions,
obtaining the session from custom parts of the HTTP request (i.e. HTTP headers),
and managing multiple user's sessions in a single browser instance (i.e. multiple authenticated accounts similar to Google).
* WebSocket - provides the ability to keep the HttpSession alive when receiving WebSocket messages
[[samples]]
= Samples and Guides (Start Here)
If you are looking to get started with Spring Session, the best place to start is our Sample Applications.
.Sample Applications
|===
| Source | Description | Guide
| {gh-samples-url}httpsession[HttpSession]
| Demonstrates how to use Spring Session to replace the `HttpSession` with a Redis store.
| link:guides/httpsession.html[HttpSession Guide]
| {gh-samples-url}boot[Spring Boot]
| Demonstrates how to use Spring Session with Spring Boot.
| link:guides/boot.html[Spring Boot]
| {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]
| {gh-samples-url}rest[REST]
| Demonstrates how to use Spring Session in a REST application to support authenticating with a header.
| TBD
| {gh-samples-url}users[Multiple Sessions]
| Demonstrates how to use Spring Session to manage multiple simultaneous browser sessions (i.e Google Accounts).
| link:guides/security.html[Users Guide]
| {gh-samples-url}websocket[WebSocket]
| Demonstrates how to use Spring Session with WebSockets.
| link:guides/websocket.html[WebSocket Guide]
[[samples-hazelcast]]
| {gh-samples-url}hazelcast[Hazelcast]
| Demonstrates how to use Spring Session with Hazelcast.
| TBD
|===
[[httpsession]]
= HttpSession Integration
Spring Session provides transparent integration with `HttpSession`.
This means that developers can switch the `HttpSession` implementation out with an implementation that is backed by Spring Session.
== HttpSession Usage
Using Spring Session with `HttpSession` is enabled by adding a Servlet Filter before anything that uses the `HttpSession`.
The <<samples, HttpSession Sample>> provides a working sample on how to integrate Spring Session and `HttpSession`.
You can the basic steps for integration below, but you are encouraged to follow along with the detailed HttpSession Guide when integrating with your own application:
include::guides/httpsession.adoc[tags=config,leveloffset=+1]
[[httpsession-how]]
== How HttpSession Integration Works
Fortunately both `HttpSession` and `HttpServletRequest` (the API for obtaining an `HttpSession`) are both interfaces.
This means that we can provide our own implementations for each of these APIs.
First we create a custom `HttpServletRequest` that returns a custom implementation of `HttpSession'.
It looks something like the following:
[source, java]
----
public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
public SessionRepositoryRequestWrapper(HttpServletRequest original) {
super(original);
}
public HttpSession getSession() {
return getSession(true);
}
public HttpSession getSession(boolean createNew) {
// create an HttpSession implementation from Spring Session
}
// ... other methods delegate to the original HttpServletRequest ...
}
----
Any method that returns an `HttpSession` is overridden.
All other methods are implemented by `HttpServletRequestWrapper` and simply delegate to the original `HttpServletRequest` implementation.
We replace the `HttpServletRequest` implementation using a servlet `Filter` called `SessionRepositoryFilter`.
The pseudocode can be found below:
[source, java]
----
public class SessionRepositoryFilter implements Filter {
public doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
SessionRepositoryRequestWrapper customRequest =
new SessionRepositoryRequestWrapper(httpRequest);
chain.doFilter(customRequest, response, chain);
}
// ...
}
----
By passing in a custom `HttpServletRequest` implementation into the `FilterChain` we ensure that anything invoked after our `Filter` uses the custom `HttpSession` implementation.
This highlights why it is important that Spring Session's `SessionRepositoryFilter` must be placed before anything that interacts with the `HttpSession`.
== Multiple Sessions in Single Browser
Spring Session has the ability to support multiple sessions in a single browser instance.
This provides the ability to support authenticating with multiple users in the same browser instance (i.e. Google Accounts).
The <<samples,Multiple Sessions Sample>> provides a complete working example of managing multiple users in the same browser instance.
include::guides/users.adoc[tags=how-does-it-work]
[[websocket]]
= WebSocket Integration
Spring Session provides transparent integration with Spring's WebSocket support.
include::guides/websocket.adoc[tags=disclaimer]
[[websocket-why]]
== Why Spring Session & WebSockets?
So why do we need Spring Session when using WebSockets?
Consider an email application that does much of its work through HTTP requests.
However, there is also a chat application embedded within it that works over WebSocket APIs.
If a user is actively chatting with someone, we should not timeout the `HttpSession` since this would be pretty poor user experience.
However, this is exactly what https://java.net/jira/browse/WEBSOCKET_SPEC-175[JSR-356] does.
Another issue is that according to JSR-356 if the `HttpSession` times out any WebSocket that was created with that HttpSession and an authenticated user should be forcibly closed.
This means that if we are actively chatting in our application and are not using the HttpSession, then we will also disconnect from our conversation!
[[websocket-usage]]
== WebSocket Usage
The <<samples, WebSocket Sample>> provides a working sample on how to integrate Spring Session with WebSockets.
You can the basic steps for integration below, but you are encouraged to follow along with the detailed WebSocket Guide when integrating with your own application:
=== HttpSession Integration
Before using WebSocket integration, you should be sure that you have <<httpsession>> working first.
include::guides/websocket.adoc[tags=config,leveloffset=+1]
= API Documentation
== Session
A `Session` is a simplified `Map` of name value pairs.
Typical usage might look like the following:
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=repository-demo]
----
<1> We create a `SessionRepository` instance with a generic type, `S`, that extends `Session`. The generic type is defined in our class.
<2> We create a new `Session` using our `SessionRepository` and assign it to a variable of type `S`.
<3> We interact with the `Session`. In our example, we demonstrate saving a `User` to the `Session`.
<4> We now save the `Session`. This is why we needed the generic type `S`. The `SessionRepository` only allows saving `Session` instances that were created or retrieved using the same `SessionRepository`. This allows for the `SessionRepository` to make implementation specific optimizations (i.e. only writing attributes that have changed).
<5> We retrieve the `Session` from the `SessionRepository`.
<6> We obtain the persisted `User` from our `Session` without the need for explicitly casting our attribute.
== ExpiringSession
An `ExpiringSession` extends a `Session` by providing attributes related to the `Session` instance's expiration.
If there is no need to interact with the expiration information, prefer using the more simple `Session` API.
Typical usage might look like the following:
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=expire-repository-demo]
----
<1> We create a `SessionRepository` instance with a generic type, `S`, that extends `ExpiringSession`. The generic type is defined in our class.
<2> We create a new `ExpiringSession` using our `SessionRepository` and assign it to a variable of type `S`.
<3> We interact with the `ExpiringSession`.
In our example, we demonstrate updating the amount of time the `ExpiringSession` can be inactive before it expires.
<4> We now save the `ExpiringSession`.
This is why we needed the generic type `S`.
The `SessionRepository` only allows saving `ExpiringSession` instances that were created or retrieved using the same `SessionRepository`.
This allows for the `SessionRepository` to make implementation specific optimizations (i.e. only writing attributes that have changed).
The last accessed time is automatically updated when the `ExpiringSession` is saved.
<5> We retrieve the `ExpiringSession` from the `SessionRepository`.
If the `ExpiringSession` were expired, the result would be null.
== SessionRepository
A `SessionRepository` is in charge of creating, retrieving, and persisting `Session` instances.
If possible, developers should not interact directly with a `SessionRepository` or a `Session`.
Instead, developers should prefer interacting with `SessionRepository` and `Session` indirectly through the <<httpsession,HttpSession>> and <<websocket,WebSocket>> integration.
== RedisOperationsSessionRepository
`RedisOperationsSessionRepository` is a `SessionRepository` that is implemented using Spring Data's `RedisOperations`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
The implementation supports `SessionDestroyedEvent` through `SessionMessageListener`.
=== Instantiating a RedisOperationsSessionRepository
A typical example of how to create a new instance can be seen below:
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-redisoperationssessionrepository]
----
For additional information on how to create a `RedisConnectionFactory`, refer to the Spring Data Redis Reference.
=== Storage Details
Each session is stored in Redis as a Hash.
Each session is set and updated using the HMSET command.
An example of how each session is stored can be seen below.
HMSET spring:session:sessions:<session-id> creationTime 1404360000000 \
maxInactiveInterval 1800 lastAccessedTime 1404360000000 \
sessionAttr:<attrName> someAttrValue sessionAttr:<attrName2> someAttrValue2
==== Session Expiration
An expiration is associated to each session using the EXPIRE command based upon the RedisOperationsSessionRepository.RedisSession.getMaxInactiveInterval().
For example:
EXPIRE spring:session:sessions:<session-id> 1800
Each session expiration is also tracked to the nearest minute.
This allows a background task to cleanup expired sessions in a deterministic fashion.
For example:
SADD spring:session:expirations:<expire-rounded-up-to-nearest-minute> <session-id>
EXPIRE spring:session:expirations:<expire-rounded-up-to-nearest-minute> 1800
The Redis expiration is still placed on each key to ensure that if the server is down when the session expires, it is still cleaned up.
==== Optimized Writes
The `Session` instances managed by `RedisOperationsSessionRepository` keeps track of the properties that have changed and only updates those.
This means if an attribute is written once and read many times we only need to write that attribute once.
For example, assume the session attribute "sessionAttr2" from earlier was updated.
The following would be executed upon saving:
HMSET spring:session:sessions:<session-id> sessionAttr:<attrName2> newValue
EXPIRE spring:session:sessions:<session-id> 1800
=== SessionDestroyedEvent
`RedisOperationsSessionRepository` supports firing a `SessionDestroyedEvent` whenever a `Session` is deleted or when it expires.
This is necessary to ensure resources associated with the `Session` are properly cleaned up.
For example, when integrating with WebSockets the `SessionDestroyedEvent` is in charge of closing any active WebSocket connections.
Firing a `SessionDestroyedEvent` is made available through the `SessionMessageListener` which listens to http://redis.io/topics/notifications[Redis Keyspace events].
In order for this to work, Redis Keyspace events for Generic commands and Expired events needs to be enabled.
For example:
TIP: If you are using `@EnableRedisHttpSession` the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
[source,bash]
----
redis-cli config set notify-keyspace-events Egx
----
=== Viewing the Session in Redis
After http://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis http://redis.io/commands#hash[using the redis-cli].
For example, enter the following into a terminal:
[source,bash]
----
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" <1>
2) "spring:session:expirations:1418772300000" <2>
----
<1> The suffix of this key is the session identifier of the Spring Session.
<2> This key contains all the session ids that should be deleted at the time `1418772300000`.
You can also view the attributes of each session.
[source,bash]
----
redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"
----
== MapSessionRepository
The `MapSessionRepository` allows for persisting `ExpiringSession` in a `Map` with the key being the `ExpiringSession` id and the value being the `ExpiringSession`.
The implementation can be used with a `ConcurrentHashMap` as a testing or convenience mechanism.
Alternatively, it can be used with distributed `Map` implementations. For example, it can be used with Hazelcast.
=== Instantiating MapSessionRepository
Creating a new instance is as simple as:
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-mapsessionrepository]
----
=== Using Spring Session and Hazlecast
The <<samples,Hazelcast Sample>> is a complete application demonstrating using Spring Session with Hazelcast.
To run it use the following:
./gradlew :samples:hazelcast:tomcatRun
= Spring Session Community
We are glad to consider you a part of our community.
Please find additional information below.
== Support
You can get help by asking questions on http://stackoverflow.com/questions/tagged/spring-session[StackOverflow with the tag spring-session].
Similarly we encourage helping others by answering questions on StackOverflow.
== Source Code
Our source code can be found on github at https://github.com/spring-projects/spring-session/
== Issue Tracking
We track issues in github issues at https://github.com/spring-projects/spring-session/issues
== Contributing
We appreciate https://help.github.com/articles/using-pull-requests/[Pull Requests].
== License
Spring Session is Open Source software released under the http://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2002-2014 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 docs;
import org.junit.Test;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.*;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import static org.fest.assertions.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class IndexDocTests {
static final String ATTR_USER = "user";
@Test
public void repositoryDemo() {
ExpiringRepositoryDemo<ExpiringSession> demo = new ExpiringRepositoryDemo<ExpiringSession>();
demo.repository = new MapSessionRepository();
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = repository.createSession(); // <2>
// <3>
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
repository.save(toSave); // <4>
S session = repository.getSession(toSave.getId()); // <5>
// <6>
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
public void expireRepositoryDemo() {
ExpiringRepositoryDemo<ExpiringSession> demo = new ExpiringRepositoryDemo<ExpiringSession>();
demo.repository = new MapSessionRepository();
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends ExpiringSession> {
private SessionRepository<S> repository; // <1>
public void demo() {
S toSave = repository.createSession(); // <2>
// ...
toSave.setMaxInactiveInterval(30); // <3>
repository.save(toSave); // <4>
S session = repository.getSession(toSave.getId()); // <5>
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
public void newRedisOperationsSessionRepository() {
// tag::new-redisoperationssessionrepository[]
JedisConnectionFactory factory = new JedisConnectionFactory();
SessionRepository<? extends ExpiringSession> repository =
new RedisOperationsSessionRepository(factory);
// end::new-redisoperationssessionrepository[]
}
@Test
public void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends ExpiringSession> repository = new MapSessionRepository();
// end::new-mapsessionrepository[]
}
private static class User {
private User(String username) {}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2014 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 docs.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
/**
* @author Rob Winch
*/
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig
extends AbstractWebSocketMessageBrokerConfigurer {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messages")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
}
// end::class[]

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2002-2014 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 docs.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
/**
* @author Rob Winch
*/
public class WebSocketDocTests {
}

View File

@@ -24,11 +24,11 @@ import org.springframework.session.data.redis.config.annotation.web.http.EnableR
* @author Rob Winch
*/
@Configuration
@EnableRedisHttpSession
@EnableRedisHttpSession // <1>
public class HttpSessionConfig {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
return new JedisConnectionFactory(); // <2>
}
}

View File

@@ -15,10 +15,17 @@
*/
package sample;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.EnumSet;
import java.util.Map;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
@@ -26,19 +33,10 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.EnumSet;
import java.util.Map;
@WebListener
public class Initializer implements ServletContextListener {
@@ -64,8 +62,11 @@ public class Initializer implements ServletContextListener {
instance = Hazelcast.newHazelcastInstance(cfg);
Map<String,ExpiringSession> sessions = instance.getMap(sessionMapName);
SessionRepository<ExpiringSession> sessionRepository = new MapSessionRepository(sessions);
Dynamic fr = sc.addFilter("springSessionFilter", new SessionRepositoryFilter<ExpiringSession>(sessionRepository ));
SessionRepository<ExpiringSession> sessionRepository =
new MapSessionRepository(sessions);
SessionRepositoryFilter<ExpiringSession> filter =
new SessionRepositoryFilter<ExpiringSession>(sessionRepository);
Dynamic fr = sc.addFilter("springSessionFilter", filter);
fr.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}

View File

@@ -1,4 +1,3 @@
package sample;
/*
* Copyright 2002-2014 the original author or authors.
*
@@ -14,6 +13,7 @@ package sample;
* License for the specific language governing permissions and limitations under
* the License.
*/
package sample;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -24,13 +24,13 @@ import org.springframework.session.data.redis.config.annotation.web.http.EnableR
/**
* @author Rob Winch
*/
@Import(EmbeddedRedisConfiguration.class)
@Configuration
@EnableRedisHttpSession
@Import(EmbeddedRedisConfiguration.class) // <1>
@EnableRedisHttpSession // <2>
public class Config {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
return new JedisConnectionFactory(); // <3>
}
}

View File

@@ -1,4 +1,3 @@
package sample;
/*
* Copyright 2002-2014 the original author or authors.
*
@@ -14,7 +13,7 @@ package sample;
* License for the specific language governing permissions and limitations under
* the License.
*/
package sample;
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
@@ -23,7 +22,4 @@ import org.springframework.session.web.context.AbstractHttpSessionApplicationIni
*/
public class Initializer extends AbstractHttpSessionApplicationInitializer {
public Initializer() {
super(Config.class, SecurityConfig.class);
}
}

View File

@@ -20,5 +20,10 @@ import org.springframework.security.web.context.AbstractSecurityWebApplicationIn
/**
* @author Rob Winch
*/
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
public class SecurityInitializer extends
AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(SecurityConfig.class, Config.class);
}
}

View File

@@ -40,18 +40,20 @@ public class UserAccountsFilter implements Filter {
@SuppressWarnings("unchecked")
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletRequest httpRequest = (HttpServletRequest) request;
// tag::HttpSessionManager[]
HttpSessionManager sessionManager =
(HttpSessionManager) req.getAttribute(HttpSessionManager.class.getName());
(HttpSessionManager) httpRequest.getAttribute(HttpSessionManager.class.getName());
// end::HttpSessionManager[]
SessionRepository<Session> repo =
(SessionRepository<Session>) req.getAttribute(SessionRepository.class.getName());
(SessionRepository<Session>) httpRequest.getAttribute(SessionRepository.class.getName());
String currentSessionAlias = sessionManager.getCurrentSessionAlias(req);
Map<String, String> sessionIds = sessionManager.getSessionIds(req);
String newSessionAlias = String.valueOf(System.currentTimeMillis());
String currentSessionAlias = sessionManager.getCurrentSessionAlias(httpRequest);
Map<String, String> sessionIds = sessionManager.getSessionIds(httpRequest);
String unauthenticatedAlias = null;
String contextPath = req.getContextPath();
String contextPath = httpRequest.getContextPath();
List<Account> accounts = new ArrayList<Account>();
Account currentAccount = null;
for(Map.Entry<String, String> entry : sessionIds.entrySet()) {
@@ -65,7 +67,7 @@ public class UserAccountsFilter implements Filter {
String username = session.getAttribute("username");
if(username == null) {
newSessionAlias = alias;
unauthenticatedAlias = alias;
continue;
}
@@ -79,9 +81,16 @@ public class UserAccountsFilter implements Filter {
}
}
req.setAttribute("currentAccount", currentAccount);
req.setAttribute("addAccountUrl", sessionManager.encodeURL(contextPath, newSessionAlias));
req.setAttribute("accounts", accounts);
// tag::addAccountUrl[]
String addAlias = unauthenticatedAlias == null ? // <1>
sessionManager.getNewSessionAlias(httpRequest) : // <2>
unauthenticatedAlias; // <3>
String addAccountUrl = sessionManager.encodeURL(contextPath, addAlias); // <4>
// end::addAccountUrl[]
httpRequest.setAttribute("currentAccount", currentAccount);
httpRequest.setAttribute("addAccountUrl", addAccountUrl);
httpRequest.setAttribute("accounts", accounts);
chain.doFilter(request, response);
}

View File

@@ -22,9 +22,14 @@
<ul class="nav navbar-nav">
<c:url value="/" var="homeUrl"/>
<li class="active"><a id="navHome" href="${homeUrl}">Home</a></li>
<c:url value="/link.jsp" var="linkUrl"/>
<li><a id="navLink" href="${linkUrl}">Link</a></li>
<li>
<!-- tag::link[]
-->
<c:url value="/link.jsp" var="linkUrl"/>
<a id="navLink" href="${linkUrl}">Link</a>
<!-- end::link[]
-->
</li>
</ul>
<c:if test="${currentAccount != null or not empty accounts}">
<ul class="nav navbar-nav navbar-right">

View File

@@ -31,9 +31,11 @@ import org.springframework.session.data.redis.config.annotation.web.http.EnableR
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableRedisHttpSession
// tag::enable-redis-httpsession[]
@EnableRedisHttpSession//(maxInactiveIntervalInSeconds = 60)
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
// end::enable-redis-httpsession[]
@Override
protected void configure(HttpSecurity http) throws Exception {

View File

@@ -1,9 +1,22 @@
/*
* Copyright 2002-2014 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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
@@ -11,34 +24,21 @@ import org.springframework.session.web.socket.config.annotation.AbstractSessionW
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import sample.data.ActiveWebSocketUserRepository;
import sample.websocket.WebSocketConnectHandler;
import sample.websocket.WebSocketDisconnectHandler;
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S> {
public class WebSocketConfig
extends AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> { // <1>
@Bean
public WebSocketConnectHandler<S> webSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
return new WebSocketConnectHandler<S>(messagingTemplate, repository);
}
@Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
protected void configureStompEndpoints(StompEndpointRegistry registry) { // <2>
registry.addEndpoint("/messages")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
@Bean
public WebSocketDisconnectHandler<S> webSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
return new WebSocketDisconnectHandler<S>(messagingTemplate, repository);
}
}
}
// end::class[]

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2014 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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.session.ExpiringSession;
import sample.data.ActiveWebSocketUserRepository;
import sample.websocket.WebSocketConnectHandler;
import sample.websocket.WebSocketDisconnectHandler;
/**
* These handlers are separated from WebSocketConfig because they are specific to this application and do not demonstrate a typical Spring Session setup.
*
* @author Rob Winch
*/
@Configuration
public class WebSocketHandlersConfig<S extends ExpiringSession> {
@Bean
public WebSocketConnectHandler<S> webSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
return new WebSocketConnectHandler<S>(messagingTemplate, repository);
}
@Bean
public WebSocketDisconnectHandler<S> webSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
return new WebSocketDisconnectHandler<S>(messagingTemplate, repository);
}
}

View File

@@ -60,10 +60,15 @@ public class SessionMessageListener implements MessageListener {
if(!body.startsWith("spring:session:sessions:")) {
return;
}
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
if(logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
publishEvent(new SessionDestroyedEvent(this, sessionId));
}

View File

@@ -161,8 +161,6 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
this.connectionFactory = connectionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
RedisConnection connection = connectionFactory.getConnection();
String notifyOptions = getNotifyOptions(connection);

View File

@@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
@@ -180,6 +181,29 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
return u;
}
public String getNewSessionAlias(HttpServletRequest request) {
Set<String> sessionAliases = getSessionIds(request).keySet();
if(sessionAliases.isEmpty()) {
return DEFAULT_ALIAS;
}
long lastAlias = Long.decode(DEFAULT_ALIAS);
for(String alias : sessionAliases) {
long selectedAlias = safeParse(alias);
if(selectedAlias > lastAlias) {
lastAlias = selectedAlias;
}
}
return Long.toHexString(lastAlias + 1);
}
private long safeParse(String hex) {
try {
return Long.decode("0x" + hex);
} catch(NumberFormatException notNumber) {
return 0;
}
}
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
@@ -293,6 +317,9 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
}
while(tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if(!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}

View File

@@ -59,4 +59,18 @@ public interface HttpSessionManager {
* @return the encoded URL
*/
String encodeURL(String url, String sessionAlias);
/**
* Gets a new and unique Session alias. Typically this will be called to pass into
* {@code HttpSessionManager#encodeURL(java.lang.String)}. For example:
*
* <code>
* String newAlias = httpSessionManager.getNewSessionAlias(request);
* String addAccountUrl = httpSessionManager.encodeURL("./", newAlias);
* </code>
*
* @param request
* @return
*/
String getNewSessionAlias(HttpServletRequest request);
}

View File

@@ -8,9 +8,9 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import javax.servlet.http.Cookie;
import java.util.Map;
public class CookieHttpSessionStrategyTests {
private MockHttpServletRequest request;
@@ -36,14 +36,14 @@ public class CookieHttpSessionStrategyTests {
@Test
public void getRequestedSessionIdNotNull() throws Exception {
setSessionId(session.getId());
setSessionCookie(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@Test
public void getRequestedSessionIdNotNullCustomCookieName() throws Exception {
setCookieName("CUSTOM");
setSessionId(session.getId());
setSessionCookie(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@@ -56,7 +56,7 @@ public class CookieHttpSessionStrategyTests {
@Test
public void onNewSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionId(existing.getId());
setSessionCookie(existing.getId());
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
@@ -64,7 +64,7 @@ public class CookieHttpSessionStrategyTests {
@Test
public void onNewSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionId(existing.getId());
setSessionCookie(existing.getId());
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo("0 " + existing.getId() + " new " + session.getId());
@@ -111,7 +111,7 @@ public class CookieHttpSessionStrategyTests {
@Test
public void onDeleteSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionId("0 " + existing.getId() + " new " + session.getId());
setSessionCookie("0 " + existing.getId() + " new " + session.getId());
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
@@ -119,7 +119,7 @@ public class CookieHttpSessionStrategyTests {
@Test
public void onDeleteSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionId("0 " + existing.getId() + " new " + session.getId());
setSessionCookie("0 " + existing.getId() + " new " + session.getId());
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEqualTo(existing.getId());
@@ -282,8 +282,6 @@ public class CookieHttpSessionStrategyTests {
assertThat(strategy.getCurrentSessionAlias(request)).isEqualTo("01234567890123456789012345678901234567890123456789");
}
@Test
public void getCurrentSession() {
String expectedAlias = "1";
@@ -291,16 +289,126 @@ public class CookieHttpSessionStrategyTests {
assertThat(strategy.getCurrentSessionAlias(request)).isEqualTo(expectedAlias);
}
// --- getNewSessionAlias
@Test
public void getNewSessionAliasNoSessions() {
assertThat(strategy.getNewSessionAlias(request)).isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getNewSessionAliasSingleSession() {
setSessionCookie("abc");
assertThat(strategy.getNewSessionAlias(request)).isEqualTo("1");
}
@Test
public void getNewSessionAlias2Sessions() {
setCookieWithNSessions(2);
assertThat(strategy.getNewSessionAlias(request)).isEqualTo("2");
}
@Test
public void getNewSessionAlias9Sessions() {
setCookieWithNSessions(9);
assertThat(strategy.getNewSessionAlias(request)).isEqualToIgnoringCase("9");
}
@Test
public void getNewSessionAlias10Sessions() {
setCookieWithNSessions(10);
assertThat(strategy.getNewSessionAlias(request)).isEqualToIgnoringCase("a");
}
@Test
public void getNewSessionAlias16Sessions() {
setCookieWithNSessions(16);
assertThat(strategy.getNewSessionAlias(request)).isEqualToIgnoringCase("10");
}
@Test
public void getNewSessionAliasInvalidAlias() {
setSessionCookie("0 1 $ b");
assertThat(strategy.getNewSessionAlias(request)).isEqualToIgnoringCase("1");
}
// --- getSessionIds
@Test
public void getSessionIdsNone() {
assertThat(strategy.getSessionIds(request)).isEmpty();
}
@Test
public void getSessionIdsSingle() {
String expectedId = "a";
setSessionCookie(expectedId);
Map<String, String> sessionIds = strategy.getSessionIds(request);
assertThat(sessionIds.size()).isEqualTo(1);
assertThat(sessionIds.get("0")).isEqualTo(expectedId);
}
@Test
public void getSessionIdsMulti() {
setSessionCookie("0 a 1 b");
Map<String, String> sessionIds = strategy.getSessionIds(request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
}
@Test
public void getSessionIdsDangling() {
setSessionCookie("0 a 1 b noValue");
Map<String, String> sessionIds = strategy.getSessionIds(request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
}
// --- helper
@Test
public void createSessionCookieValue() {
assertThat(createSessionCookieValue(17)).isEqualToIgnoringCase("0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 a 10 b 11 c 12 d 13 e 14 f 15 10 16");
}
private void setCookieWithNSessions(long size) {
setSessionCookie(createSessionCookieValue(size));
}
private String createSessionCookieValue(long size) {
StringBuffer buffer = new StringBuffer();
for(long i=0;i < size; i++) {
String hex = Long.toHexString(i);
buffer.append(hex);
buffer.append(" ");
buffer.append(i);
if(i < size - 1) {
buffer.append(" ");
}
}
return buffer.toString();
}
public void setCookieName(String cookieName) {
strategy.setCookieName(cookieName);
this.cookieName = cookieName;
}
public void setSessionId(String id) {
request.setCookies(new Cookie(cookieName, id));
public void setSessionCookie(String value) {
request.setCookies(new Cookie(cookieName, value));
}
public String getSessionId() {