Add JdbcOperationsSessionRepository

This commit provides implementation of SessionRepository based
on Spring's JdbcOperations interface.

@EnableJdbcHttpSession annotation is provided to ease the
configuration, together with spring-session-jdbc BOM and schema
creation scripts for all major databases.

Fixes gh-364
This commit is contained in:
Vedran Pavic
2016-02-22 22:47:23 +01:00
committed by Rob Winch
parent 091d0d8d9f
commit cd38e307e0
37 changed files with 2363 additions and 1 deletions

View File

@@ -0,0 +1,20 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_6_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarRunner {
skipProject = true
}
dependencies {
compile project(':spring-session-jdbc'),
"org.springframework:spring-web:$springVersion",
"com.h2database:h2:1.4.191",
jstlDependencies
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"
testCompile "junit:junit:$junitVersion"
integrationTestCompile gebDependencies
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample
import geb.spock.*
import sample.pages.HomePage;
import spock.lang.Stepwise
import pages.*
/**
* Tests the CAS sample application using service tickets.
*
* @author Rob Winch
*/
@Stepwise
class AttributeTests extends GebReportingSpec {
def 'first visit no attributes'() {
when:
to HomePage
then:
attributes.empty
}
def 'create attribute'() {
when:
createAttribute('a','b')
then:
attributes.size() == 1
attributes[0].name == 'a'
attributes[0].value == 'b'
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Session Attributes'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
createAttribute(required:false) { name, value ->
form.attributeName = name
form.attributeValue = value
submit.click(HomePage)
}
attributes { moduleList AttributeRow, $("table tr").tail() }
}
}
class AttributeRow extends Module {
static content = {
cell { $("td", it) }
name { cell(0).text() }
value { cell(1).text() }
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
// tag::class[]
public class SessionServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String attributeName = req.getParameter("attributeName");
String attributeValue = req.getParameter("attributeValue");
req.getSession().setAttribute(attributeName, attributeValue);
resp.sendRedirect(req.getContextPath() + "/");
}
private static final long serialVersionUID = 2878267318695777395L;
}
// end::class[]

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<!-- tag::beans[] -->
<!--1-->
<context:annotation-config/>
<bean class="org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration"/>
<!--2-->
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:org/springframework/session/jdbc/schema-h2.sql"/>
</jdbc:embedded-database>
<!--3-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>
<!-- end::beans[] -->
</beans>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
-->
<!-- tag::context-param[] -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*.xml
</param-value>
</context-param>
<!-- end::context-param[] -->
<!-- tag::springSessionRepositoryFilter[] -->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- end::springSessionRepositoryFilter[] -->
<!--
- Loads the root application context of this web app at startup.
- The application context is then available via
- WebApplicationContextUtils.getWebApplicationContext(servletContext).
-->
<!-- tag::listeners[] -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- end::listeners[] -->
<servlet>
<servlet-name>session</servlet-name>
<servlet-class>sample.SessionServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>session</servlet-name>
<url-pattern>/session</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Session Attributes</title>
<link rel="stylesheet" href="assets/bootstrap.min.css">
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<h1>Description</h1>
<p>This application demonstrates how to use a relational database to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.</p>
<h1>Try it</h1>
<form class="form-inline" role="form" action="./session" method="post">
<label for="attributeName">Attribute Name</label>
<input id="attributeName" type="text" name="attributeName"/>
<label for="attributeValue">Attribute Value</label>
<input id="attributeValue" type="text" name="attributeValue"/>
<input type="submit" value="Set Attribute"/>
</form>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>Attribute Name</th>
<th>Attribute Value</th>
</tr>
</thead>
<tbody>
<c:forEach items="${sessionScope}" var="attr">
<tr>
<td><c:out value="${attr.key}"/></td>
<td><c:out value="${attr.value}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_7_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarRunner {
skipProject = true
}
dependencies {
compile project(':spring-session-jdbc'),
"org.springframework:spring-web:$springVersion",
"com.h2database:h2:1.4.191",
jstlDependencies
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"
testCompile "junit:junit:$junitVersion"
integrationTestCompile gebDependencies
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample
import geb.spock.*
import sample.pages.HomePage;
import spock.lang.Stepwise
import pages.*
/**
* Tests the CAS sample application using service tickets.
*
* @author Rob Winch
*/
@Stepwise
class AttributeTests extends GebReportingSpec {
def 'first visit no attributes'() {
when:
to HomePage
then:
attributes.empty
}
def 'create attribute'() {
when:
createAttribute('a','b')
then:
attributes.size() == 1
attributes[0].name == 'a'
attributes[0].value == 'b'
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Session Attributes'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
createAttribute(required:false) { name, value ->
form.attributeName = name
form.attributeValue = value
submit.click(HomePage)
}
attributes { moduleList AttributeRow, $("table tr").tail() }
}
}
class AttributeRow extends Module {
static content = {
cell { $("td", it) }
name { cell(0).text() }
value { cell(1).text() }
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
import org.springframework.transaction.PlatformTransactionManager;
// tag::class[]
@EnableJdbcHttpSession // <1>
public class Config {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder() // <2>
.setType(EmbeddedDatabaseType.H2)
.addScript("org/springframework/session/jdbc/schema-h2.sql")
.build();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource); // <3>
}
}
// end::class[]

View File

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

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.io.IOException;
// tag::class[]
@WebServlet("/session")
public class SessionServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String attributeName = req.getParameter("attributeName");
String attributeValue = req.getParameter("attributeValue");
req.getSession().setAttribute(attributeName, attributeValue);
resp.sendRedirect(req.getContextPath() + "/");
}
private static final long serialVersionUID = 2878267318695777395L;
}
// tag::end[]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Session Attributes</title>
<link rel="stylesheet" href="assets/bootstrap.min.css">
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<h1>Description</h1>
<p>This application demonstrates how to use a relational database to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.</p>
<h1>Try it</h1>
<form class="form-inline" role="form" action="./session" method="post">
<label for="attributeName">Attribute Name</label>
<input id="attributeName" type="text" name="attributeName"/>
<label for="attributeValue">Attribute Value</label>
<input id="attributeValue" type="text" name="attributeValue"/>
<input type="submit" value="Set Attribute"/>
</form>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>Attribute Name</th>
<th>Attribute Value</th>
</tr>
</thead>
<tbody>
<c:forEach items="${sessionScope}" var="attr">
<tr>
<td><c:out value="${attr.key}"/></td>
<td><c:out value="${attr.value}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -12,6 +12,8 @@ include 'samples:httpsession-gemfire-clientserver'
include 'samples:httpsession-gemfire-clientserver-xml'
include 'samples:httpsession-gemfire-p2p'
include 'samples:httpsession-gemfire-p2p-xml'
include 'samples:httpsession-jdbc'
include 'samples:httpsession-jdbc-xml'
include 'samples:httpsession-xml'
include 'samples:rest'
include 'samples:security'
@@ -21,3 +23,4 @@ include 'samples:websocket'
include 'spring-session'
include 'spring-session-data-gemfire'
include 'spring-session-data-redis'
include 'spring-session-jdbc'

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 JDBC"
dependencies {
compile project(':spring-session'),
"org.springframework:spring-jdbc:$springVersion"
}
dependencyManagement {
springIoTestRuntime {
imports {
mavenBom "io.spring.platform:platform-bom:${springIoVersion}"
}
}
}

View File

@@ -18,6 +18,7 @@ dependencies {
optional "org.springframework.data:spring-data-redis:$springDataRedisVersion",
"com.hazelcast:hazelcast:$hazelcastVersion",
"org.springframework.data:spring-data-gemfire:$springDataGemFireVersion",
"org.springframework:spring-jdbc:$springVersion",
"org.springframework:spring-context:$springVersion",
"org.springframework:spring-web:$springVersion",
"org.springframework:spring-messaging:$springVersion",
@@ -25,7 +26,8 @@ dependencies {
provided "javax.servlet:javax.servlet-api:$servletApiVersion"
integrationTestCompile "redis.clients:jedis:2.4.1",
"org.apache.commons:commons-pool2:2.2",
"com.hazelcast:hazelcast-client:$hazelcastVersion"
"com.hazelcast:hazelcast-client:$hazelcastVersion",
"com.h2database:h2:1.4.191"
integrationTestRuntime "org.springframework.shell:spring-shell:1.0.0.RELEASE"

View File

@@ -0,0 +1,468 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc;
import java.util.Map;
import java.util.UUID;
import javax.sql.DataSource;
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;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
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.MapSession;
import org.springframework.session.Session;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link JdbcOperationsSessionRepository}.
*
* @author Vedran Pavic
* @since 1.2.0
*/
@WebAppConfiguration
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class JdbcOperationsSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
private JdbcOperationsSessionRepository repository;
private SecurityContext context;
private SecurityContext changedContext;
@Before
public void setup() throws Exception {
this.context = SecurityContextHolder.createEmptyContext();
this.context.setAuthentication(new UsernamePasswordAuthenticationToken(
"username-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
this.changedContext = SecurityContextHolder.createEmptyContext();
this.changedContext.setAuthentication(new UsernamePasswordAuthenticationToken(
"changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER")));
}
@Test
public void saves() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
JdbcOperationsSessionRepository.JdbcSession toSave = this.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);
this.repository.save(toSave);
Session session = this.repository.getSession(toSave.getId());
assertThat(session.getId()).isEqualTo(toSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames());
assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(toSave.getAttribute(expectedAttributeName));
this.repository.delete(toSave.getId());
assertThat(this.repository.getSession(toSave.getId())).isNull();
}
@Test
public void putAllOnSingleAttrDoesNotRemoveOld() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute("a", "b");
this.repository.save(toSave);
toSave = this.repository.getSession(toSave.getId());
toSave.setAttribute("1", "2");
this.repository.save(toSave);
toSave = this.repository.getSession(toSave.getId());
Session session = this.repository.getSession(toSave.getId());
assertThat(session.getAttributeNames().size()).isEqualTo(2);
assertThat(session.getAttribute("a")).isEqualTo("b");
assertThat(session.getAttribute("1")).isEqualTo("2");
this.repository.delete(toSave.getId());
}
@Test
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
this.repository.delete(toSave.getId());
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
public void findByPrincipalNameExpireRemovesIndex() throws Exception {
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
toSave.setLastAccessedTime(System.currentTimeMillis() -
(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000));
this.repository.save(toSave);
this.repository.cleanUpExpiredSessions();
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.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();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.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();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave = this.repository.getSession(toSave.getId());
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.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();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute(INDEX_NAME, null);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedPrincipalName() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
toSave.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.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();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedPrincipalNameReload() throws Exception {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findBySecurityPrincipalName() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
this.repository.delete(toSave.getId());
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
public void findBySecurityPrincipalNameExpireRemovesIndex() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
toSave.setLastAccessedTime(System.currentTimeMillis() -
(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000));
this.repository.save(toSave);
this.repository.cleanUpExpiredSessions();
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
}
@Test
public void findByPrincipalNameNoSecurityPrincipalNameChange() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByPrincipalNameNoSecurityPrincipalNameChangeReload() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave = this.repository.getSession(toSave.getId());
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedSecurityPrincipalName() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, null);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedSecurityPrincipalName() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
@Test
public void findByDeletedSecurityPrincipalNameReload() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.getSession(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
public void findByChangedSecurityPrincipalNameReload() throws Exception {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.getSession(toSave.getId());
getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName =
this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
}
private String getSecurityName() {
return this.context.getAuthentication().getName();
}
private String getChangedSecurityName() {
return this.changedContext.getAuthentication().getName();
}
@Configuration
@EnableJdbcHttpSession
static class Config {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("org/springframework/session/jdbc/schema-h2.sql")
.build();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
}

View File

@@ -0,0 +1,476 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link org.springframework.session.SessionRepository} implementation that uses
* Spring's {@link JdbcOperations} to store sessions in a relational database. This
* implementation does not support publishing of session events.
* <p>
* An example of how to create a new instance can be seen below:
*
* <pre class="code">
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
*
* JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(jdbcTemplate);
* </pre>
*
* For additional information on how to create and configure a JdbcTemplate, refer to the
* <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/">
* Spring Framework Reference Documentation</a>.
* <p>
* By default, this implementation uses <code>SPRING_SESSION</code> table to store
* sessions. Note that the table name can be customized using the
* {@link #setTableName(String)} method.
*
* Depending on your database, the table definition can be described as below:
*
* <pre class="code">
* CREATE TABLE SPRING_SESSION (
* SESSION_ID CHAR(36),
* LAST_ACCESS_TIME BIGINT NOT NULL,
* PRINCIPAL_NAME VARCHAR(100),
* SESSION_BYTES BLOB,
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
* );
* </pre>
*
* Due to the differences between the various database vendors, especially when it comes
* to storing binary data, make sure to use SQL script specific to your database. Scripts
* for most major database vendors are packaged as
* <code>org/springframework/session/jdbc/schema-*.sql</code>, where <code>*</code> is the
* target database type.
*
* @author Vedran Pavic
* @since 1.2.0
*/
public class JdbcOperationsSessionRepository
implements FindByIndexNameSessionRepository<JdbcOperationsSessionRepository.JdbcSession> {
private static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
private static final String CREATE_SESSION_QUERY =
"INSERT INTO %TABLE_NAME%(SESSION_ID, LAST_ACCESS_TIME, PRINCIPAL_NAME, SESSION_BYTES) VALUES (?, ?, ?, ?)";
private static final String GET_SESSION_QUERY =
"SELECT SESSION_BYTES FROM %TABLE_NAME% WHERE SESSION_ID = ?";
private static final String UPDATE_SESSION_QUERY =
"UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ?, PRINCIPAL_NAME = ?, SESSION_BYTES = ? WHERE SESSION_ID = ?";
private static final String UPDATE_SESSION_LAST_ACCESS_TIME_QUERY =
"UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ? WHERE SESSION_ID = ?";
private static final String DELETE_SESSION_QUERY =
"DELETE FROM %TABLE_NAME% WHERE SESSION_ID = ?";
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY =
"SELECT SESSION_BYTES FROM %TABLE_NAME% WHERE PRINCIPAL_NAME = ?";
private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY =
"DELETE FROM %TABLE_NAME% WHERE LAST_ACCESS_TIME < ?";
private static final Log logger = LogFactory.getLog(JdbcOperationsSessionRepository.class);
private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
private final JdbcOperations jdbcOperations;
private final RowMapper<ExpiringSession> mapper = new ExpiringSessionMapper();
/**
* The name of database table used by Spring Session to store sessions.
*/
private String tableName = DEFAULT_TABLE_NAME;
/**
* If non-null, this value is used to override the default value for
* {@link JdbcSession#setMaxInactiveIntervalInSeconds(int)}.
*/
private Integer defaultMaxInactiveInterval;
private Converter<Object, byte[]> serializingConverter = new SerializingConverter();
private Converter<byte[], Object> deserializingConverter = new DeserializingConverter();
private LobHandler lobHandler = new DefaultLobHandler();
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* default ${JdbcOperations} to manage sessions.
* @param dataSource the {@link DataSource} to use
*/
public JdbcOperationsSessionRepository(DataSource dataSource) {
this(createDefaultTemplate(dataSource));
}
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided ${JdbcOperations} to manage sessions.
* @param jdbcOperations the {@link JdbcOperations} to use
*/
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
this.jdbcOperations = jdbcOperations;
}
/**
* Set the name of database table used to store sessions.
* @param tableName the database table name
*/
public void setTableName(String tableName) {
Assert.hasText(tableName, "Table name must not be empty");
this.tableName = tableName.trim();
}
/**
* Set the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
*/
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
public void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "LobHandler must not be null");
this.lobHandler = lobHandler;
}
public void setSerializingConverter(Converter<Object, byte[]> serializingConverter) {
Assert.notNull(serializingConverter, "SerializingConverter must not be null");
this.serializingConverter = serializingConverter;
}
public void setDeserializingConverter(Converter<byte[], Object> deserializingConverter) {
Assert.notNull(deserializingConverter, "DeserializingConverter must not be null");
this.deserializingConverter = deserializingConverter;
}
@Override
public JdbcSession createSession() {
JdbcSession session = new JdbcSession();
if (this.defaultMaxInactiveInterval != null) {
session.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
}
return session;
}
@Override
public void save(final JdbcSession session) {
if (session.isNew()) {
this.jdbcOperations.update(getQuery(CREATE_SESSION_QUERY), new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime());
ps.setString(3, session.getPrincipalName());
JdbcOperationsSessionRepository.this.lobHandler.getLobCreator()
.setBlobAsBytes(ps, 4, serialize(session.delegate));
}
});
}
else {
if (session.isAttributesChanged()) {
this.jdbcOperations.update(getQuery(UPDATE_SESSION_QUERY), new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getPrincipalName());
JdbcOperationsSessionRepository.this.lobHandler.getLobCreator()
.setBlobAsBytes(ps, 3, serialize(session.delegate));
ps.setString(4, session.getId());
}
});
}
else if (session.isLastAccessTimeChanged()) {
this.jdbcOperations.update(getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY), new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setLong(1, session.getLastAccessedTime());
ps.setString(2, session.getId());
}
});
}
else {
return;
}
}
session.clearChangeFlags();
}
@Override
public JdbcSession getSession(String id) {
ExpiringSession session = null;
try {
session = this.jdbcOperations.queryForObject(getQuery(GET_SESSION_QUERY),
new Object[] { id }, this.mapper);
}
catch (EmptyResultDataAccessException ignored) {
}
if (session != null) {
if (session.isExpired()) {
delete(id);
}
else {
return new JdbcSession(session);
}
}
return null;
}
@Override
public void delete(String id) {
this.jdbcOperations.update(getQuery(DELETE_SESSION_QUERY), id);
}
@Override
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<ExpiringSession> sessions = this.jdbcOperations.query(
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY), new Object[] { indexValue }, this.mapper);
Map<String, JdbcSession> sessionMap = new HashMap<String, JdbcSession>(sessions.size());
for (ExpiringSession session : sessions) {
sessionMap.put(session.getId(), new JdbcSession(session));
}
return sessionMap;
}
@Scheduled(cron = "0 * * * * *")
public void cleanUpExpiredSessions() {
long now = System.currentTimeMillis();
long roundedNow = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(roundedNow));
}
int deletedCount = this.jdbcOperations.update(
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY), roundedNow);
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
private static JdbcTemplate createDefaultTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.afterPropertiesSet();
return jdbcTemplate;
}
protected String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private byte[] serialize(ExpiringSession session) {
return this.serializingConverter.convert(session);
}
private static long roundDownMinute(long timeInMs) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs);
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}
final class JdbcSession implements ExpiringSession {
private final ExpiringSession delegate;
private boolean isNew;
private boolean lastAccessTimeChanged;
private boolean attributesChanged;
public JdbcSession() {
this.delegate = new MapSession();
this.isNew = true;
}
public JdbcSession(ExpiringSession delegate) {
Assert.notNull("ExpiringSession cannot be null");
this.delegate = delegate;
}
public boolean isNew() {
return this.isNew;
}
public boolean isLastAccessTimeChanged() {
return this.lastAccessTimeChanged;
}
public boolean isAttributesChanged() {
return this.attributesChanged;
}
public void clearChangeFlags() {
this.isNew = false;
this.lastAccessTimeChanged = false;
this.attributesChanged = false;
}
public String getPrincipalName() {
return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
}
@Override
public long getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public void setLastAccessedTime(long lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.lastAccessTimeChanged = true;
}
@Override
public long getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveIntervalInSeconds(int interval) {
this.delegate.setMaxInactiveIntervalInSeconds(interval);
this.attributesChanged = true;
}
@Override
public int getMaxInactiveIntervalInSeconds() {
return this.delegate.getMaxInactiveIntervalInSeconds();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public <T> T getAttribute(String attributeName) {
return this.delegate.getAttribute(attributeName);
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.attributesChanged = true;
}
@Override
public void removeAttribute(String attributeName) {
this.delegate.removeAttribute(attributeName);
this.attributesChanged = true;
}
}
static class PrincipalNameResolver {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}
private class ExpiringSessionMapper implements RowMapper<ExpiringSession> {
@Override
public ExpiringSession mapRow(ResultSet rs, int rowNum) throws SQLException {
return (ExpiringSession) JdbcOperationsSessionRepository.this.deserializingConverter.convert(
JdbcOperationsSessionRepository.this.lobHandler.getBlobAsBytes(rs, "SESSION_BYTES"));
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc.config.annotation.web.http;
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;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
/**
* Add this annotation to an {@code @Configuration} class to expose the
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and
* backed by a relational database. In order to leverage the annotation, a single
* {@link javax.sql.DataSource} must be provided. For example:
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableJdbcHttpSession
* public class JdbcHttpSessionConfig {
*
* &#064;Bean
* public DataSource dataSource() {
* return new EmbeddedDatabaseBuilder()
* .setType(EmbeddedDatabaseType.H2)
* .addScript("org/springframework/session/jdbc/schema-h2.sql")
* .build();
* }
*
* &#064;Bean
* public PlatformTransactionManager transactionManager(DataSource dataSource) {
* return new DataSourceTransactionManager(dataSource);
* }
*
* }
* </pre>
*
* More advanced configurations can extend {@link JdbcHttpSessionConfiguration} instead.
*
* @author Vedran Pavic
* @since 1.2.0
* @see EnableSpringHttpSession
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE })
@Documented
@Import(JdbcHttpSessionConfiguration.class)
@Configuration
public @interface EnableJdbcHttpSession {
/**
* The name of database table used by Spring Session to store sessions.
* @return the database table name
*/
String tableName() default "";
/**
* The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
* This should be a non-negative integer.
*
* @return the seconds a session can be inactive before expiring
*/
int maxInactiveIntervalInSeconds() default 1800;
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc.config.annotation.web.http;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.convert.converter.Converter;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.util.StringUtils;
/**
* Spring @Configuration class used to configure and initialize a JDBC based HttpSession
* provider implementation in Spring Session.
* <p>
* Exposes the {@link org.springframework.session.web.http.SessionRepositoryFilter} as a
* bean named "springSessionRepositoryFilter". In order to use this a single
* {@link DataSource} must be exposed as a Bean.
*
* @author Vedran Pavic
* @since 1.2.0
* @see EnableJdbcHttpSession
*/
@Configuration
@EnableScheduling
public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware {
private String tableName = "";
private Integer maxInactiveIntervalInSeconds = 1800;
private LobHandler lobHandler;
private Converter<Object, byte[]> serializingConverter;
private Converter<byte[], Object> deserializingConverter;
@Bean
public JdbcTemplate springSessionJdbcOperations(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcOperationsSessionRepository sessionRepository(
@Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations) {
JdbcOperationsSessionRepository sessionRepository =
new JdbcOperationsSessionRepository(jdbcOperations);
String tableName = getTableName();
if (StringUtils.hasText(tableName)) {
sessionRepository.setTableName(tableName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
}
if (this.serializingConverter != null) {
sessionRepository.setSerializingConverter(this.serializingConverter);
}
if (this.deserializingConverter != null) {
sessionRepository.setDeserializingConverter(this.deserializingConverter);
}
return sessionRepository;
}
@Autowired(required = false)
@Qualifier("springSessionLobHandler")
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
@Autowired(required = false)
@Qualifier("springSessionSerializingConverter")
public void setSerializingConverter(Converter<Object, byte[]> serializingConverter) {
this.serializingConverter = serializingConverter;
}
@Autowired(required = false)
@Qualifier("springSessionDeserializingConverter")
public void setDeserializingConverter(Converter<byte[], Object> deserializingConverter) {
this.deserializingConverter = deserializingConverter;
}
private String getTableName() {
if (StringUtils.hasText(this.tableName)) {
return this.tableName;
}
return System.getProperty("spring.session.jdbc.tableName", "");
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata.getAnnotationAttributes(EnableJdbcHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
this.tableName = enableAttrs.getString("tableName");
this.maxInactiveIntervalInSeconds = enableAttrs.getNumber("maxInactiveIntervalInSeconds");
}
}

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES BLOB,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES BLOB,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES LONGVARBINARY,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES LONGVARBINARY,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES BLOB,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
) ENGINE=InnoDB;
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME NUMBER(19,0) NOT NULL,
PRINCIPAL_NAME VARCHAR2(100 CHAR),
SESSION_BYTES BLOB,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES BYTEA,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHARACTER(36),
LAST_ACCESS_TIME INTEGER NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES BLOB,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES IMAGE,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,9 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
LAST_ACCESS_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
SESSION_BYTES IMAGE,
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
) LOCK DATAROWS;
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

View File

@@ -0,0 +1,317 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Matchers.startsWith;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link JdbcOperationsSessionRepository}.
*
* @author Vedran Pavic
* @since 1.2.0
*/
@RunWith(MockitoJUnitRunner.class)
public class JdbcOperationsSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private DataSource dataSource;
@Mock
private JdbcOperations jdbcOperations;
private JdbcOperationsSessionRepository repository;
@Before
public void setUp() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
}
@Test
public void constructorDataSource() {
JdbcOperationsSessionRepository repository =
new JdbcOperationsSessionRepository(this.dataSource);
assertThat(ReflectionTestUtils.getField(repository, "jdbcOperations")).isNotNull();
}
@Test
public void constructorNullDataSource() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Property 'dataSource' is required");
new JdbcOperationsSessionRepository((DataSource) null);
}
@Test
public void constructorNullJdbcOperations() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("JdbcOperations must not be null");
new JdbcOperationsSessionRepository((JdbcOperations) null);
}
@Test
public void setTableNameNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Table name must not be empty");
this.repository.setTableName(null);
}
@Test
public void setTableNameEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Table name must not be empty");
this.repository.setTableName(" ");
}
@Test
public void setLobHandlerNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LobHandler must not be null");
this.repository.setLobHandler(null);
}
@Test
public void setSerializingConverterNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SerializingConverter must not be null");
this.repository.setSerializingConverter(null);
}
@Test
public void setDeserializingConverterNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("DeserializingConverter must not be null");
this.repository.setDeserializingConverter(null);
}
@Test
public void createSessionDefaultMaxInactiveInterval() throws Exception {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(
new MapSession().getMaxInactiveIntervalInSeconds());
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void createSessionCustomMaxInactiveInterval() throws Exception {
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(interval);
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void saveNew() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"), isA(PreparedStatementSetter.class));
}
@Test
public void saveUpdatedAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
session.setAttribute("testName", "testValue");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1))
.update(and(startsWith("UPDATE"), contains("SESSION_BYTES")), isA(PreparedStatementSetter.class));
}
@Test
public void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
session.setLastAccessedTime(System.currentTimeMillis());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1))
.update(and(startsWith("UPDATE"), not(contains("SESSION_BYTES"))), isA(PreparedStatementSetter.class));
}
@Test
public void saveUnchanged() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void getSessionNotFound() {
String sessionId = "testSessionId";
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(sessionId);
assertThat(session).isNull();
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { sessionId }), isA(RowMapper.class));
}
@Test
public void getSessionExpired() {
MapSession expired = new MapSession();
expired.setMaxInactiveIntervalInSeconds(0);
when(this.jdbcOperations.queryForObject(startsWith("SELECT"), eq(new Object[] { expired.getId() }), isA(RowMapper.class)))
.thenReturn(expired);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(expired.getId());
assertThat(session).isNull();
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { expired.getId() }), isA(RowMapper.class));
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(expired.getId()));
}
@Test
public void getSessionFound() {
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
when(this.jdbcOperations.queryForObject(startsWith("SELECT"), eq(new Object[] { saved.getId() }), isA(RowMapper.class)))
.thenReturn(saved);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.isNew()).isFalse();
assertThat(session.getAttribute("savedName")).isEqualTo("savedValue");
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { saved.getId() }), isA(RowMapper.class));
}
@Test
public void delete() {
String sessionId = "testSessionId";
this.repository.delete(sessionId);
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId));
}
@Test
public void findByIndexNameAndIndexValueUnknownIndexName() {
String indexValue = "testIndexValue";
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions =
this.repository.findByIndexNameAndIndexValue("testIndexName", indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
String principal = "username";
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
verify(this.jdbcOperations, times(1)).query(
startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class));
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, "notused", AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<MapSession>(2);
MapSession saved1 = new MapSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
MapSession saved2 = new MapSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
when(this.jdbcOperations.query(startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class)))
.thenReturn(saved);
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
verify(this.jdbcOperations, times(1)).query(
startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class));
}
@Test
public void cleanupExpiredSessions() {
this.repository.cleanUpExpiredSessions();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong());
}
}

View File

@@ -0,0 +1,224 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.jdbc.config.annotation.web.http;
import javax.sql.DataSource;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JdbcHttpSessionConfiguration}.
*
* @author Vedran Pavic
* @since 1.2.0
*/
public class JdbcHttpSessionConfigurationTests {
private static final String TABLE_NAME = "TEST_SESSION";
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String TABLE_NAME_SYSTEM_PROPERTY = "spring.session.jdbc.tableName";
@Rule
public final ExpectedException thrown = ExpectedException.none();
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void noDataSourceConfiguration() {
this.thrown.expect(UnsatisfiedDependencyException.class);
this.thrown.expectMessage("springSessionJdbcOperations");
registerAndRefresh(EmptyConfiguration.class);
}
@Test
public void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(JdbcOperationsSessionRepository.class)).isNotNull();
}
@Test
public void customTableName() {
registerAndRefresh(CustomTableNameConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
@Test
public void customTableNameSystemProperty() {
System.setProperty(TABLE_NAME_SYSTEM_PROPERTY, TABLE_NAME);
try {
registerAndRefresh(DefaultConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
finally {
System.clearProperty(TABLE_NAME_SYSTEM_PROPERTY);
}
}
@Test
public void customMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
public void customLobHandlerConfiguration() {
registerAndRefresh(CustomLobHandlerConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
LobHandler lobHandler = this.context.getBean(LobHandler.class);
assertThat(repository).isNotNull();
assertThat(lobHandler).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "lobHandler")).isEqualTo(lobHandler);
}
@Test
@SuppressWarnings("unchecked")
public void customSerializingConverterConfiguration() {
registerAndRefresh(CustomSerializingConverterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
Converter<Object, byte[]> serializingConverter =
(Converter<Object, byte[]>) this.context.getBean("springSessionSerializingConverter");
assertThat(repository).isNotNull();
assertThat(serializingConverter).isNotNull();
Converter<Object, byte[]> sc =
(Converter<Object, byte[]>) ReflectionTestUtils.getField(repository, "serializingConverter");
assertThat(sc).isEqualTo(serializingConverter);
}
@Test
@SuppressWarnings("unchecked")
public void customDeserializingConverterConfiguration() {
registerAndRefresh(CustomDeserializingConverterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
Converter<byte[], Object> deserializingConverter =
(Converter<byte[], Object>) this.context.getBean("springSessionDeserializingConverter");
assertThat(repository).isNotNull();
assertThat(deserializingConverter).isNotNull();
Converter<byte[], Object> dc =
(Converter<byte[], Object>) ReflectionTestUtils.getField(repository, "deserializingConverter");
assertThat(dc).isEqualTo(deserializingConverter);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
@Configuration
@EnableJdbcHttpSession
static class EmptyConfiguration {
}
static class BaseConfiguration {
@Bean
public DataSource dataSource() {
return mock(DataSource.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession(tableName = TABLE_NAME)
static class CustomTableNameConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class CustomMaxInactiveIntervalInSecondsConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession
static class CustomLobHandlerConfiguration extends BaseConfiguration {
@Bean
public LobHandler springSessionLobHandler() {
return mock(LobHandler.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class CustomSerializingConverterConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
public Converter<Object, byte[]> springSessionSerializingConverter() {
return mock(Converter.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class CustomDeserializingConverterConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
public Converter<byte[], Object> springSessionDeserializingConverter() {
return mock(Converter.class);
}
}
}