diff --git a/README.adoc b/README.adoc index fa4c227e..2d769854 100644 --- a/README.adoc +++ b/README.adoc @@ -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 diff --git a/build.gradle b/build.gradle index 1cc0662d..8388629c 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 00000000..6183e9c1 --- /dev/null +++ b/docs/build.gradle @@ -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 +} \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/boot.adoc b/docs/src/docs/asciidoc/guides/boot.adoc new file mode 100644 index 00000000..b0580315 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/boot.adoc @@ -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 <>. + +== 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"] +---- + + + + + org.springframework.session + spring-session-data-redis + {spring-session-version} + pom + + +---- + +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] +---- + + + + + + spring-snapshot + 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: + +.pom.xml +[source,xml] +---- + + spring-milestone + https://repo.spring.io/libs-milestone + +---- +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 <> 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. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/httpsession.adoc b/docs/src/docs/asciidoc/guides/httpsession.adoc new file mode 100644 index 00000000..f0a65884 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/httpsession.adoc @@ -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 <>. + +// 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"] +---- + + + + + org.springframework.session + spring-session-data-redis + {spring-session-version} + pom + + + org.springframework + spring-web + {spring-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: + +.pom.xml +[source,xml] +---- + + + + + + spring-snapshot + 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: + +.pom.xml +[source,xml] +---- + + spring-milestone + https://repo.spring.io/libs-milestone + +---- +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 <> 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. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/security.adoc b/docs/src/docs/asciidoc/guides/security.adoc new file mode 100644 index 00000000..18729334 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/security.adoc @@ -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 <>. + +== 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"] +---- + + + + + org.springframework.session + spring-session-data-redis + {spring-session-version} + pom + + + org.springframework + spring-web + {spring-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: + +.pom.xml +[source,xml] +---- + + + + + + spring-snapshot + 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: + +.pom.xml +[source,xml] +---- + + spring-milestone + https://repo.spring.io/libs-milestone + +---- +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 <> 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. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/users.adoc b/docs/src/docs/asciidoc/guides/users.adoc new file mode 100644 index 00000000..bdfc482f --- /dev/null +++ b/docs/src/docs/asciidoc/guides/users.adoc @@ -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] +---- +Link +---- + +end::how-does-it-work[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/websocket.adoc b/docs/src/docs/asciidoc/guides/websocket.adoc new file mode 100644 index 00000000..31455e3a --- /dev/null +++ b/docs/src/docs/asciidoc/guides/websocket.adoc @@ -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. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..08a26ee8 --- /dev/null +++ b/docs/src/docs/asciidoc/index.adoc @@ -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 <> 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 <> 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 <> 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 <> 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 <> and <> 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: creationTime 1404360000000 \ + maxInactiveInterval 1800 lastAccessedTime 1404360000000 \ + sessionAttr: someAttrValue sessionAttr: 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: 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 spring:session:expirations: 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: sessionAttr: newValue + EXPIRE spring:session:sessions: 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 <> 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]. diff --git a/docs/src/test/java/docs/IndexDocTests.java b/docs/src/test/java/docs/IndexDocTests.java new file mode 100644 index 00000000..4f5d30fd --- /dev/null +++ b/docs/src/test/java/docs/IndexDocTests.java @@ -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 demo = new ExpiringRepositoryDemo(); + demo.repository = new MapSessionRepository(); + + demo.demo(); + } + + // tag::repository-demo[] + public class RepositoryDemo { + private SessionRepository 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 demo = new ExpiringRepositoryDemo(); + demo.repository = new MapSessionRepository(); + + demo.demo(); + } + + // tag::expire-repository-demo[] + public class ExpiringRepositoryDemo { + private SessionRepository 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 repository = + new RedisOperationsSessionRepository(factory); + // end::new-redisoperationssessionrepository[] + } + + @Test + public void mapRepository() { + // tag::new-mapsessionrepository[] + SessionRepository repository = new MapSessionRepository(); + // end::new-mapsessionrepository[] + } + + private static class User { + private User(String username) {} + } +} diff --git a/docs/src/test/java/docs/websocket/WebSocketConfig.java b/docs/src/test/java/docs/websocket/WebSocketConfig.java new file mode 100644 index 00000000..acb24de3 --- /dev/null +++ b/docs/src/test/java/docs/websocket/WebSocketConfig.java @@ -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[] \ No newline at end of file diff --git a/docs/src/test/java/docs/websocket/WebSocketDocTests.java b/docs/src/test/java/docs/websocket/WebSocketDocTests.java new file mode 100644 index 00000000..e1562757 --- /dev/null +++ b/docs/src/test/java/docs/websocket/WebSocketDocTests.java @@ -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 { + +} diff --git a/samples/boot/src/main/java/sample/config/HttpSessionConfig.java b/samples/boot/src/main/java/sample/config/HttpSessionConfig.java index bafb53ba..e4b2377c 100644 --- a/samples/boot/src/main/java/sample/config/HttpSessionConfig.java +++ b/samples/boot/src/main/java/sample/config/HttpSessionConfig.java @@ -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> } } diff --git a/samples/hazelcast/src/main/java/sample/Initializer.java b/samples/hazelcast/src/main/java/sample/Initializer.java index d4001aaf..0f99f331 100644 --- a/samples/hazelcast/src/main/java/sample/Initializer.java +++ b/samples/hazelcast/src/main/java/sample/Initializer.java @@ -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 sessions = instance.getMap(sessionMapName); - SessionRepository sessionRepository = new MapSessionRepository(sessions); - Dynamic fr = sc.addFilter("springSessionFilter", new SessionRepositoryFilter(sessionRepository )); + SessionRepository sessionRepository = + new MapSessionRepository(sessions); + SessionRepositoryFilter filter = + new SessionRepositoryFilter(sessionRepository); + Dynamic fr = sc.addFilter("springSessionFilter", filter); fr.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); } diff --git a/samples/security/src/main/java/sample/Config.java b/samples/security/src/main/java/sample/Config.java index 8bc170b4..8045c405 100644 --- a/samples/security/src/main/java/sample/Config.java +++ b/samples/security/src/main/java/sample/Config.java @@ -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> } } diff --git a/samples/security/src/main/java/sample/Initializer.java b/samples/security/src/main/java/sample/Initializer.java index e7ec846a..aac525fc 100644 --- a/samples/security/src/main/java/sample/Initializer.java +++ b/samples/security/src/main/java/sample/Initializer.java @@ -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); - } } diff --git a/samples/security/src/main/java/sample/SecurityInitializer.java b/samples/security/src/main/java/sample/SecurityInitializer.java index c56de7a0..d8c5fc34 100644 --- a/samples/security/src/main/java/sample/SecurityInitializer.java +++ b/samples/security/src/main/java/sample/SecurityInitializer.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/users/src/main/java/sample/UserAccountsFilter.java b/samples/users/src/main/java/sample/UserAccountsFilter.java index a114ba47..0a8d54f2 100644 --- a/samples/users/src/main/java/sample/UserAccountsFilter.java +++ b/samples/users/src/main/java/sample/UserAccountsFilter.java @@ -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 repo = - (SessionRepository) req.getAttribute(SessionRepository.class.getName()); + (SessionRepository) httpRequest.getAttribute(SessionRepository.class.getName()); - String currentSessionAlias = sessionManager.getCurrentSessionAlias(req); - Map sessionIds = sessionManager.getSessionIds(req); - String newSessionAlias = String.valueOf(System.currentTimeMillis()); + String currentSessionAlias = sessionManager.getCurrentSessionAlias(httpRequest); + Map sessionIds = sessionManager.getSessionIds(httpRequest); + String unauthenticatedAlias = null; - String contextPath = req.getContextPath(); + String contextPath = httpRequest.getContextPath(); List accounts = new ArrayList(); Account currentAccount = null; for(Map.Entry 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); } diff --git a/samples/users/src/main/webapp/index.jsp b/samples/users/src/main/webapp/index.jsp index 9fc51845..45e655ac 100644 --- a/samples/users/src/main/webapp/index.jsp +++ b/samples/users/src/main/webapp/index.jsp @@ -22,9 +22,14 @@