Add MongoOperationsSessionRepository

Fixes gh-17
This commit is contained in:
Jakub Kubrynski
2016-03-07 10:13:50 -06:00
committed by Rob Winch
parent 7b28b214ff
commit 34cebc3df6
35 changed files with 9139 additions and 24 deletions

View File

@@ -0,0 +1,167 @@
= Spring Session - Mongo Repositories
Jakub Kubrynski
:toc:
This guide describes how to use Spring Session backed by Mongo.
NOTE: The completed guide can be found in the <<mongo-sample, mongo 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-mongo</artifactId>
<version>{spring-session-version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
----
ifeval::["{version-snapshot}" == "true"]
Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository.
Ensure you have the following in your pom.xml:
.pom.xml
[source,xml]
----
<repositories>
<!-- ... -->
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
----
endif::[]
ifeval::["{version-milestone}" == "true"]
Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository.
Ensure you have the following in your pom.xml:
.pom.xml
[source,xml]
----
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
----
endif::[]
[[mongo-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.
// tag::config[]
All you have to do is to add the following Spring Configuration:
[source,java]
----
include::{samples-dir}mongo/src/main/java/sample/config/HttpSessionConfig.java[tags=class]
----
<1> The `@EnableMongoHttpSession` 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 Mongo.
// end::config[]
[[boot-mongo-configuration]]
== Configuring the Mongo Connection
Spring Boot automatically creates a `MongoClient` that connects Spring Session to a Mongo Server on localhost on port 27017 (default port).
In a production environment you need to ensure to update your configuration to point to your Mongo server.
For example, you can include the following in your *application.properties*
.src/main/resources/application.properties
----
spring.data.mongodb.host=mongo-srv
spring.data.mongodb.port=27018
spring.data.mongodb.database=prod
----
For more information, refer to http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-connecting-to-mongodb[Connecting to MongoDB] portion of the Spring Boot documentation.
[[boot-servlet-configuration]]
== 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.
[[mongo-sample]]
== Mongo Sample Application
The Mongo Sample Application demonstrates how to use Spring Session to transparently leverage Mongo to back a web application's `HttpSession` when using Spring Boot.
[[mongo-running]]
=== Running the Mongo Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
[NOTE]
====
For the sample to work, you must have MongoDB on localhost and run it with the default port (27017).
Alternatively you can use docker to run local instance `docker run -p 27017:27017 mongo`
====
----
$ ./gradlew :samples:mongo:bootRun
----
You should now be able to access the application at http://localhost:8080/
[[boot-explore]]
=== Exploring the security Sample Application
Try using the application. Enter the following to log in:
* **Username** _user_
* **Password** _password_
Now click the **Login** button.
You should now see a message indicating your are logged in with the user entered previously.
The user's information is stored in Mongo rather than Tomcat's `HttpSession` implementation.
[[mongo-how]]
=== How does it work?
Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Mongo.
Spring Session replaces the `HttpSession` with an implementation that is backed by Mongo.
When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Mongo.
When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session.
Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]).
If you like, you can easily remove the session using mongo client. For example, on a Linux based system you can type:
$ mongo
> use test
> db.sessions.find().pretty()
Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `60f17293-839b-477c-bb92-07a9c3658843` with the value of your SESSION cookie:
> db.sessions.remove({"_id":"60f17293-839b-477c-bb92-07a9c3658843"})
Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated.

View File

@@ -99,6 +99,10 @@ If you are looking to get started with Spring Session, the best place to start i
| Demonstrates how to use Spring Session with WebSockets.
| link:guides/websocket.html[WebSocket Guide]
| {gh-samples-url}mongo[Mongo]
| Demonstrates how to use Spring Session with Mongo.
| link:guides/mongo.html[Mongo Guide]
[[samples-hazelcast]]
| {gh-samples-url}hazelcast[Hazelcast]
| Demonstrates how to use Spring Session with Hazelcast.
@@ -259,6 +263,18 @@ Guide when integrating with your own application.
include::guides/httpsession-gemfire-p2p-xml.adoc[tags=config,leveloffset=+3]
[[httpsession-mongo]]
=== HttpSession with Mongo
Using Spring Session with `HttpSession` is enabled by adding a Servlet Filter before anything that uses the `HttpSession`.
This section describes how to use Mongo to back `HttpSession` using Java based configuration.
NOTE: The <<samples, HttpSession Mongo Sample>> provides a working sample on how to integrate Spring Session and `HttpSession` using Java configuration.
You can read 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/mongo.adoc[tags=config,leveloffset=+3]
[[httpsession-how]]
=== How HttpSession Integration Works

View File

@@ -20,3 +20,4 @@ assertjVersion=2.3.0
spockVersion=1.0-groovy-2.4
jstlVersion=1.2.1
groovyVersion=2.4.4
springDataMongoVersion=1.8.2.RELEASE

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample
import geb.spock.*
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.IntegrationTest
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import sample.pages.HomePage
import sample.pages.LoginPage
import spock.lang.Stepwise
import pages.*
/**
* Tests the demo that supports multiple sessions
*
* @author Rob Winch
*/
@Stepwise
@ContextConfiguration(classes = Application, loader = SpringApplicationContextLoader)
@WebAppConfiguration
@IntegrationTest
class BootTests extends GebReportingSpec {
def 'Unauthenticated user sent to log in page'() {
when: 'unauthenticated user request protected page'
via HomePage
then: 'sent to the log in page'
at LoginPage
}
def 'Log in views home page'() {
when: 'log in successfully'
login()
then: 'sent to original page'
at HomePage
and: 'the username is displayed'
username == 'user'
and: 'Spring Session Management is being used'
driver.manage().cookies.find { it.name == 'SESSION' }
and: 'Standard Session is NOT being used'
!driver.manage().cookies.find { it.name == 'JSESSIONID' }
}
def 'Log out success'() {
when:
logout()
then:
at LoginPage
}
def 'Logged out user sent to log in page'() {
when: 'logged out user request protected page'
via HomePage
then: 'sent to the log in page'
at LoginPage
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Spring Session Sample - Secured Content'; true}
static content = {
username { $('#un').text() }
logout(to:LoginPage) { $('input[type=submit]').click() }
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.pages
import geb.*
/**
* The Links Page
*
* @author Rob Winch
*/
class LoginPage extends Page {
static url = '/login'
static at = { assert driver.title == 'Login Page'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
login(required:false) { user='user', pass='password' ->
form.username = user
form.password = pass
submit.click(HomePage)
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author Rob Winch
*/
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
// tag::class[]
@EnableMongoHttpSession // <1>
public class HttpSessionConfig { }
// end::class[]

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package sample.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Rob Winch
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package sample.mvc;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Controller for sending the user to the login view.
*
* @author Rob Winch
*
*/
@Controller
public class IndexController {
@RequestMapping("/")
public String index() {
return "index";
}
}

View File

@@ -0,0 +1,2 @@
spring.thymeleaf.cache=false
spring.template.cache=false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

@@ -19,8 +19,10 @@ include 'samples:rest'
include 'samples:security'
include 'samples:users'
include 'samples:websocket'
include 'samples:mongo'
include 'spring-session'
include 'spring-session-data-gemfire'
include 'spring-session-data-redis'
include 'spring-session-jdbc'
include 'spring-session-data-mongo'

View File

@@ -0,0 +1,19 @@
apply from: JAVA_GRADLE
apply from: MAVEN_GRADLE
apply plugin: 'spring-io'
description = "Aggregator for Spring Session and Spring Data Mongo"
dependencies {
compile project(':spring-session'),
"org.springframework.data:spring-data-mongodb:$springDataMongoVersion"
}
dependencyManagement {
springIoTestRuntime {
imports {
mavenBom "io.spring.platform:platform-bom:${springIoVersion}"
}
}
}

View File

@@ -19,6 +19,7 @@ dependencies {
"com.hazelcast:hazelcast:$hazelcastVersion",
"org.springframework.data:spring-data-gemfire:$springDataGemFireVersion",
"org.springframework:spring-jdbc:$springVersion",
"org.springframework.data:spring-data-mongodb:$springDataMongoVersion",
"org.springframework:spring-context:$springVersion",
"org.springframework:spring-web:$springVersion",
"org.springframework:spring-messaging:$springVersion",

View File

@@ -0,0 +1,43 @@
package org.springframework.session.data;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.UUID;
/**
* Base class for repositories integration tests
*
* @author Jakub Kubrynski
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public abstract class AbstractITests {
protected SecurityContext context;
protected SecurityContext changedContext;
@Autowired(required = false)
protected SessionEventRegistry registry;
@Before
public void setup() {
if (registry != null) {
registry.clear();
}
context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
changedContext = SecurityContextHolder.createEmptyContext();
changedContext.setAuthentication(new UsernamePasswordAuthenticationToken("changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
}
}

View File

@@ -0,0 +1,410 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo;
import com.mongodb.MongoClient;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.AbstractITests;
import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession;
import org.springframework.test.context.ContextConfiguration;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jakub Kubrynski
*/
@ContextConfiguration
public class MongoRepositoryITests extends AbstractITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
protected MongoOperationsSessionRepository repository;
@Test
public void saves() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
MongoExpiringSession toSave = repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, toSaveContext);
toSave.setAttribute(INDEX_NAME, username);
repository.save(toSave);
Session session = repository.getSession(toSave.getId());
assertThat(session.getId()).isEqualTo(toSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames());
assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(toSave.getAttribute(expectedAttributeName));
repository.delete(toSave.getId());
String id = toSave.getId();
assertThat(repository.getSession(id)).isNull();
}
@Test
public void putAllOnSingleAttrDoesNotRemoveOld() {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute("a", "b");
repository.save(toSave);
toSave = repository.getSession(toSave.getId());
toSave.setAttribute("1", "2");
repository.save(toSave);
toSave = repository.getSession(toSave.getId());
Session session = repository.getSession(toSave.getId());
assertThat(session.getAttributeNames().size()).isEqualTo(2);
assertThat(session.getAttribute("a")).isEqualTo("b");
assertThat(session.getAttribute("1")).isEqualTo("2");
repository.delete(toSave.getId());
}
@Test
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
repository.delete(toSave.getId());
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
public void findByPrincipalNameNoPrincipalNameChange() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByPrincipalNameNoPrincipalNameChangeReload() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
toSave = repository.getSession(toSave.getId());
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedPrincipalName() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute(INDEX_NAME, null);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedPrincipalName() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute(INDEX_NAME, principalNameChanged);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedPrincipalNameReload() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
MongoExpiringSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
repository.save(getSession);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedPrincipalNameReload() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
repository.save(toSave);
MongoExpiringSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, principalNameChanged);
repository.save(getSession);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findBySecurityPrincipalName() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
repository.delete(toSave.getId());
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
public void findByPrincipalNameNoSecurityPrincipalNameChange() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByPrincipalNameNoSecurityPrincipalNameChangeReload() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
toSave = repository.getSession(toSave.getId());
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedSecurityPrincipalName() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, null);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedSecurityPrincipalName() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, changedContext);
repository.save(toSave);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedSecurityPrincipalNameReload() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
MongoExpiringSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
repository.save(getSession);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getChangedSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedSecurityPrincipalNameReload() throws Exception {
MongoExpiringSession toSave = repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, context);
repository.save(toSave);
MongoExpiringSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(SPRING_SECURITY_CONTEXT, changedContext);
repository.save(getSession);
Map<String, MongoExpiringSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void loadExpiredSession() throws Exception {
//given
MongoExpiringSession expiredSession = repository.createSession();
long thirtyOneMinutesAgo = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31);
expiredSession.setLastAccessedTime(thirtyOneMinutesAgo);
repository.save(expiredSession);
//then
MongoExpiringSession expiredSessionFromDb = repository.getSession(expiredSession.getId());
assertThat(expiredSessionFromDb).isNull();
}
private String getSecurityName() {
return context.getAuthentication().getName();
}
private String getChangedSecurityName() {
return changedContext.getAuthentication().getName();
}
@Configuration
@EnableMongoHttpSession
static class Config {
@Bean
public MongoOperations mongoOperations() throws UnknownHostException {
return new MongoTemplate(new MongoClient(), "test");
}
}
}

View File

@@ -20,9 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import java.util.Map;
import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -36,19 +34,16 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.AbstractITests;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class RedisOperationsSessionRepositoryITests {
public class RedisOperationsSessionRepositoryITests extends AbstractITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@@ -56,26 +51,9 @@ public class RedisOperationsSessionRepositoryITests {
@Autowired
private RedisOperationsSessionRepository repository;
@Autowired
private SessionEventRegistry registry;
@Autowired
RedisOperations<Object, Object> redis;
SecurityContext context;
SecurityContext changedContext;
@Before
public void setup() {
registry.clear();
context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new UsernamePasswordAuthenticationToken("username-"+UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
changedContext = SecurityContextHolder.createEmptyContext();
changedContext.setAuthentication(new UsernamePasswordAuthenticationToken("changedContext-"+UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
}
@Test
public void saves() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();

View File

@@ -0,0 +1,30 @@
package org.springframework.session.data.mongo;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Utility class to extract principal name from {@code Authentication} object
*
* @author Jakub Kubrynski
*/
class AuthenticationParser {
private static final String NAME_EXPRESSION = "authentication?.name";
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
/**
* Extracts principal name from authentication
*
* @param authentication Authentication object
* @return principal name
*/
static String extractName(Object authentication) {
if (authentication != null) {
Expression expression = PARSER.parseExpression(NAME_EXPRESSION);
return expression.getValue(authentication, String.class);
}
return null;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.session.Session;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
/**
* {@code MongoSessionConverter} implementation transforming {@code MongoExpiringSession} to/from a BSON object
* using standard Java serialization
*
* @author Jakub Kubrynski
* @since 1.2
*/
class JdkMongoSessionConverter extends MongoSessionConverter {
private static final Log LOG = LogFactory.getLog(JdkMongoSessionConverter.class);
private static final String ID = "_id";
private static final String CREATION_TIME = "created";
private static final String LAST_ACCESSED_TIME = "accessed";
private static final String MAX_INTERVAL = "interval";
private static final String ATTRIBUTES = "attr";
private static final String PRINCIPAL_FIELD_NAME = "principal";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
public Query getQueryForIndex(String indexName, Object indexValue) {
if (PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue));
}
return null;
}
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(DBObject.class, MongoExpiringSession.class));
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
if (DBObject.class.isAssignableFrom(sourceType.getType())) {
return convert((DBObject) source);
} else {
return convert((MongoExpiringSession) source);
}
}
private DBObject convert(MongoExpiringSession session) {
BasicDBObject basicDBObject = new BasicDBObject();
basicDBObject.put(ID, session.getId());
basicDBObject.put(CREATION_TIME, session.getCreationTime());
basicDBObject.put(LAST_ACCESSED_TIME, session.getLastAccessedTime());
basicDBObject.put(MAX_INTERVAL, session.getMaxInactiveIntervalInSeconds());
basicDBObject.put(PRINCIPAL_FIELD_NAME, extractPrincipal(session));
basicDBObject.put(EXPIRE_AT_FIELD_NAME, session.getExpireAt());
basicDBObject.put(ATTRIBUTES, serializeAttributes(session));
return basicDBObject;
}
private byte[] serializeAttributes(Session session) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(out);
Map<String, Object> attributes = new HashMap<String, Object>();
for (String attrName : session.getAttributeNames()) {
attributes.put(attrName, session.getAttribute(attrName));
}
outputStream.writeObject(attributes);
outputStream.flush();
return out.toByteArray();
} catch (IOException e) {
LOG.error("Exception during session serialization", e);
throw new IllegalStateException("Cannot serialize session", e);
}
}
private String extractPrincipal(Session expiringSession) {
String resolvedPrincipal = AuthenticationParser.extractName(expiringSession.getAttribute(SPRING_SECURITY_CONTEXT));
if (resolvedPrincipal != null) {
return resolvedPrincipal;
} else {
return expiringSession.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
}
}
private MongoExpiringSession convert(DBObject sessionWrapper) {
MongoExpiringSession session =
new MongoExpiringSession((String) sessionWrapper.get(ID), (Integer) sessionWrapper.get(MAX_INTERVAL));
session.setCreationTime((Long) sessionWrapper.get(CREATION_TIME));
session.setLastAccessedTime((Long) sessionWrapper.get(LAST_ACCESSED_TIME));
session.setExpireAt((Date) sessionWrapper.get(EXPIRE_AT_FIELD_NAME));
deserializeAttributes(sessionWrapper, session);
return session;
}
@SuppressWarnings("unchecked")
private void deserializeAttributes(DBObject sessionWrapper, Session session) {
try {
ByteArrayInputStream in = new ByteArrayInputStream((byte[]) sessionWrapper.get(ATTRIBUTES));
ObjectInputStream objectInputStream = new ObjectInputStream(in);
Map<String, Object> attributes = (Map<String, Object>) objectInputStream.readObject();
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
session.setAttribute(entry.getKey(), entry.getValue());
}
objectInputStream.close();
} catch (IOException e) {
LOG.error("Exception during session deserialization", e);
throw new IllegalStateException("Cannot deserialize session", e);
} catch (ClassNotFoundException e) {
LOG.error("Exception during session deserialization", e);
throw new IllegalStateException("Cannot deserialize session", e);
}
}
}

View File

@@ -0,0 +1,117 @@
package org.springframework.session.data.mongo;
import org.springframework.session.ExpiringSession;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Session object providing additional information about
* the datetime of expiration
*
* @author Jakub Kubrynski
* @since 1.2
*/
public class MongoExpiringSession implements ExpiringSession {
private final String id;
private long created = System.currentTimeMillis();
private long accessed;
private int interval;
private Date expireAt;
private Map<String, Object> attrs = new HashMap<String, Object>();
public MongoExpiringSession() {
this(MongoOperationsSessionRepository.DEFAULT_INACTIVE_INTERVAL);
}
public MongoExpiringSession(int maxInactiveIntervalInSeconds) {
this(UUID.randomUUID().toString(), maxInactiveIntervalInSeconds);
}
public MongoExpiringSession(String id, int maxInactiveIntervalInSeconds) {
this.id = id;
this.interval = maxInactiveIntervalInSeconds;
setLastAccessedTime(created);
}
public String getId() {
return id;
}
@SuppressWarnings("unchecked")
public <T> T getAttribute(String attributeName) {
return (T) attrs.get(attributeName);
}
public Set<String> getAttributeNames() {
return attrs.keySet();
}
public void setAttribute(String attributeName, Object attributeValue) {
if (attributeValue == null) {
removeAttribute(attributeName);
} else {
attrs.put(attributeName, attributeValue);
}
}
public void removeAttribute(String attributeName) {
attrs.remove(attributeName);
}
public long getCreationTime() {
return created;
}
public void setCreationTime(long created) {
this.created = created;
}
public void setLastAccessedTime(long lastAccessedTime) {
this.accessed = lastAccessedTime;
expireAt = new Date(lastAccessedTime + TimeUnit.SECONDS.toMillis(interval));
}
public long getLastAccessedTime() {
return accessed;
}
public void setMaxInactiveIntervalInSeconds(int interval) {
this.interval = interval;
}
public int getMaxInactiveIntervalInSeconds() {
return interval;
}
public boolean isExpired() {
return new Date().after(expireAt);
}
public Date getExpireAt() {
return expireAt;
}
public void setExpireAt(Date expireAt) {
this.expireAt = expireAt;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MongoExpiringSession that = (MongoExpiringSession) o;
return id.equals(that.id);
}
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo;
import com.mongodb.DBObject;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.session.FindByIndexNameSessionRepository;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Session repository implementation which stores sessions in Mongo.
* Uses {@link MongoSessionConverter} to transform session objects from/to
* native Mongo representation ({@code DBObject}).
*
* Repository is also responsible for removing expired sessions from database.
* Cleanup is done every minute.
*
* @author Jakub Kubrynski
* @since 1.2
*/
public class MongoOperationsSessionRepository implements FindByIndexNameSessionRepository<MongoExpiringSession> {
public static final int DEFAULT_INACTIVE_INTERVAL = 1800;
public static final String DEFAULT_COLLECTION_NAME = "sessions";
private final MongoOperations mongoOperations;
private MongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter();
private Integer maxInactiveIntervalInSeconds = DEFAULT_INACTIVE_INTERVAL;
private String collectionName = DEFAULT_COLLECTION_NAME;
public MongoOperationsSessionRepository(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
public MongoExpiringSession createSession() {
return new MongoExpiringSession(maxInactiveIntervalInSeconds);
}
public void save(MongoExpiringSession session) {
DBObject sessionDbObject = convertToDBObject(session);
mongoOperations.getCollection(collectionName).save(sessionDbObject);
}
public MongoExpiringSession getSession(String id) {
DBObject sessionWrapper = findSession(id);
if (sessionWrapper == null) {
return null;
}
MongoExpiringSession session = convertToSession(sessionWrapper);
if (session.isExpired()) {
delete(id);
return null;
}
return session;
}
/**
* Currently this repository allows only querying against {@code PRINCIPAL_NAME_INDEX_NAME}
*
* @param indexName the name if the index (i.e. {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
* @param indexValue the value of the index to search for.
* @return sessions map
*/
public Map<String, MongoExpiringSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
HashMap<String, MongoExpiringSession> result = new HashMap<String, MongoExpiringSession>();
Query query = mongoSessionConverter.getQueryForIndex(indexName, indexValue);
if (query == null) {
return Collections.emptyMap();
}
List<DBObject> mapSessions = mongoOperations.find(query, DBObject.class, collectionName);
for (DBObject dbSession : mapSessions) {
MongoExpiringSession mapSession = convertToSession(dbSession);
result.put(mapSession.getId(), mapSession);
}
return result;
}
public void delete(String id) {
mongoOperations.remove(findSession(id), collectionName);
}
@PostConstruct
public void ensureIndexesAreCreated() {
IndexOperations indexOperations = mongoOperations.indexOps(collectionName);
mongoSessionConverter.ensureIndexes(indexOperations);
}
DBObject findSession(String id) {
return mongoOperations.findById(id, DBObject.class, collectionName);
}
MongoExpiringSession convertToSession(DBObject session) {
return (MongoExpiringSession) mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoExpiringSession.class));
}
DBObject convertToDBObject(MongoExpiringSession session) {
return (DBObject) mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(MongoExpiringSession.class), TypeDescriptor.valueOf(DBObject.class));
}
public void setMongoSessionConverter(MongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
}

View File

@@ -0,0 +1,56 @@
package org.springframework.session.data.mongo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.query.Query;
import java.util.List;
/**
* Base class for serializing and deserializing session objects.
* To create custom serializer you have to implement this interface
* and simply register your class as a bean.
*
* @author Jakub Kubrynski
* @since 1.2
*/
public abstract class MongoSessionConverter implements GenericConverter {
private static final Log LOG = LogFactory.getLog(MongoSessionConverter.class);
protected static final String EXPIRE_AT_FIELD_NAME = "expireAt";
/**
* Returns query to be executed to return sessions based on a particular index
* @param indexName name of the index
* @param indexValue value to query against
* @return built query or null if indexName is not supported
*/
protected abstract Query getQueryForIndex(String indexName, Object indexValue);
/**
* Method ensures that there is a TTL index on {@literal expireAt} field.
* It's has {@literal expireAfterSeconds} set to zero seconds, so the expiration
* time is controlled by the application.
*
* It can be extended in custom converters when there is a need for creating
* additional custom indexes.
*/
protected void ensureIndexes(IndexOperations sessionCollectionIndexes) {
List<IndexInfo> indexInfo = sessionCollectionIndexes.getIndexInfo();
for (IndexInfo info : indexInfo) {
if (EXPIRE_AT_FIELD_NAME.equals(info.getName())) {
LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists");
return;
}
}
LOG.info("Creating TTL index on field " + EXPIRE_AT_FIELD_NAME);
sessionCollectionIndexes
.ensureIndex(new Index(EXPIRE_AT_FIELD_NAME, Sort.Direction.ASC).named(EXPIRE_AT_FIELD_NAME).expire(0));
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo.config.annotation.web.http;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.data.mongo.MongoOperationsSessionRepository;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add this annotation to a {@code @Configuration} class to expose the
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and
* backed by Mongo. Use {@code collectionName} to change default name of the
* collection used to store sessions.
* <pre>
* <code>
* {@literal @EnableMongoHttpSession}
* public class MongoHttpSessionConfig {
*
* {@literal @Bean}
* public MongoOperations mongoOperations() throws UnknownHostException {
* return new MongoTemplate(new MongoClient(), "databaseName");
* }
*
* }
* </code>
* </pre>
*
* @author Jakub Kubrynski
* @since 1.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MongoHttpSessionConfiguration.class)
@Configuration
public @interface EnableMongoHttpSession {
/**
* @return default max inactive interval in seconds
*/
int maxInactiveIntervalInSeconds() default MongoOperationsSessionRepository.DEFAULT_INACTIVE_INTERVAL;
/**
* @return name of the collection to store session
*/
String collectionName() default MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME;
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo.config.annotation.web.http;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.mongo.MongoOperationsSessionRepository;
import org.springframework.session.data.mongo.MongoSessionConverter;
/**
* Configuration class registering {@code MongoSessionRepository} bean
* To import this configuration use {@link @EnableMongoHttpSession} annotation
*
* @author Jakub Kubrynski
* @since 1.2
*/
@Configuration
class MongoHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware {
private MongoSessionConverter mongoSessionConverter;
private Integer maxInactiveIntervalInSeconds;
private String collectionName;
@Bean
MongoOperationsSessionRepository mongoSessionRepository(MongoOperations mongoOperations) {
MongoOperationsSessionRepository repository = new MongoOperationsSessionRepository(mongoOperations);
repository.setCollectionName(collectionName);
repository.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds);
if (mongoSessionConverter != null) {
repository.setMongoSessionConverter(mongoSessionConverter);
}
return repository;
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableMongoHttpSession.class.getName()));
maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
collectionName = attributes.getString("collectionName");
}
@Autowired(required = false)
public void setMongoSessionConverter(MongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.session.data.mongo;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jakub Kubrynski
*/
public class AuthenticationParserTests {
@Test
public void shouldExtractName() {
//given
String principalName = "john_the_springer";
SecurityContextImpl context = new SecurityContextImpl();
context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null));
//when
String extractedName = AuthenticationParser.extractName(context);
//then
assertThat(extractedName).isEqualTo(principalName);
}
}

View File

@@ -0,0 +1,71 @@
package org.springframework.session.data.mongo;
import com.mongodb.DBObject;
import org.junit.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jakub Kubrynski
*/
public class JdkMongoSessionConverterTests {
JdkMongoSessionConverter sut = new JdkMongoSessionConverter();
@Test
public void verifyRoundTripSerialization() throws Exception {
//given
MongoExpiringSession toSerialize = new MongoExpiringSession();
toSerialize.setAttribute("username", "john_the_springer");
//when
DBObject dbObject = convertToDBObject(toSerialize);
ExpiringSession deserialized = convertToSession(dbObject);
//then
assertThat(deserialized).isEqualToComparingFieldByField(toSerialize);
}
@Test
public void shouldExtractPrincipalNameFromAttributes() throws Exception {
//given
MongoExpiringSession toSerialize = new MongoExpiringSession();
String principalName = "john_the_springer";
toSerialize.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
//when
DBObject dbObject = convertToDBObject(toSerialize);
//then
assertThat(dbObject.get("principal")).isEqualTo(principalName);
}
@Test
public void shouldExtractPrincipalNameFromAuthentication() throws Exception {
//given
MongoExpiringSession toSerialize = new MongoExpiringSession();
String principalName = "john_the_springer";
SecurityContextImpl context = new SecurityContextImpl();
context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null));
toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context);
//when
DBObject dbObject = convertToDBObject(toSerialize);
//then
assertThat(dbObject.get("principal")).isEqualTo(principalName);
}
MongoExpiringSession convertToSession(DBObject session) {
return (MongoExpiringSession) sut.convert(session, TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoExpiringSession.class));
}
DBObject convertToDBObject(MongoExpiringSession session) {
return (DBObject) sut.convert(session, TypeDescriptor.valueOf(MongoExpiringSession.class), TypeDescriptor.valueOf(DBObject.class));
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Jakub Kubrynski
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoOperationsSessionRepositoryTests {
@Mock
MongoOperations mongoOperations;
@Mock
MongoSessionConverter converter;
MongoOperationsSessionRepository sut;
@Before
public void setUp() throws Exception {
sut = new MongoOperationsSessionRepository(mongoOperations);
sut.setMongoSessionConverter(converter);
}
@Test
public void shouldCreateSession() throws Exception {
//when
ExpiringSession session = sut.createSession();
//then
assertThat(session.getId()).isNotEmpty();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(MongoOperationsSessionRepository.DEFAULT_INACTIVE_INTERVAL);
}
@Test
public void shouldSaveSession() throws Exception {
//given
MongoExpiringSession session = new MongoExpiringSession();
BasicDBObject dbSession = new BasicDBObject();
DBCollection collection = mock(DBCollection.class);
when(converter.convert(session, TypeDescriptor.valueOf(MongoExpiringSession.class), TypeDescriptor.valueOf(DBObject.class))).thenReturn(dbSession);
when(mongoOperations.getCollection(MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)).thenReturn(collection);
//when
sut.save(session);
//then
verify(collection).save(dbSession);
}
@Test
public void shouldGetSession() throws Exception {
//given
String sessionId = UUID.randomUUID().toString();
BasicDBObject dbSession = new BasicDBObject();
when(mongoOperations.findById(sessionId, DBObject.class, MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)).thenReturn(dbSession);
MongoExpiringSession session = new MongoExpiringSession();
when(converter.convert(dbSession, TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoExpiringSession.class))).thenReturn(session);
//when
ExpiringSession retrievedSession = sut.getSession(sessionId);
//then
assertThat(retrievedSession).isEqualTo(session);
}
@Test
public void shouldHandleExpiredSession() throws Exception {
//given
String sessionId = UUID.randomUUID().toString();
BasicDBObject dbSession = new BasicDBObject();
when(mongoOperations.findById(sessionId, DBObject.class, MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)).thenReturn(dbSession);
MongoExpiringSession session = mock(MongoExpiringSession.class);
when(session.isExpired()).thenReturn(true);
when(session.getId()).thenReturn(sessionId);
when(converter.convert(dbSession, TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoExpiringSession.class))).thenReturn(session);
//when
sut.getSession(sessionId);
//then
verify(mongoOperations).remove(any(DBObject.class), eq(MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME));
}
@Test
public void shouldDeleteSession() throws Exception {
//given
String sessionId = UUID.randomUUID().toString();
//when
sut.delete(sessionId);
//then
verify(mongoOperations).remove(any(DBObject.class), eq(MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME));
}
@Test
public void shouldGetSessionsMapByPrincipal() throws Exception {
//given
String principalNameIndexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
DBObject dbSession = new BasicDBObject();
when(converter.getQueryForIndex(anyString(), Matchers.anyObject())).thenReturn(mock(Query.class));
when(mongoOperations.find(any(Query.class), eq(DBObject.class), eq(MongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)))
.thenReturn(Collections.singletonList(dbSession));
String sessionId = UUID.randomUUID().toString();
MongoExpiringSession session = new MongoExpiringSession(sessionId, 1800);
when(converter.convert(dbSession, TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoExpiringSession.class))).thenReturn(session);
//when
Map<String, MongoExpiringSession> sessionsMap = sut.findByIndexNameAndIndexValue(principalNameIndexName, "john");
//then
assertThat(sessionsMap).containsOnlyKeys(sessionId);
assertThat(sessionsMap).containsValues(session);
}
@Test
public void shouldReturnEmptyMapForNotSupportedIndex() throws Exception {
//given
String index = "some_not_supported_index_name";
//when
Map<String, MongoExpiringSession> sessionsMap = sut.findByIndexNameAndIndexValue(index, "some_value");
//then
assertThat(sessionsMap).isEmpty();
}
}