Move spring-session to spring-session-core

Issue gh-806
This commit is contained in:
Rob Winch
2017-06-16 15:48:36 -05:00
parent 6ad5006280
commit f1319483ee
94 changed files with 6 additions and 6 deletions

View File

@@ -0,0 +1,63 @@
/*
* 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.hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for Hazelcast integration tests.
*
* @author Tommy Ludwig
* @author Vedran Pavic
*/
public abstract class AbstractHazelcastRepositoryITests {
@Autowired
private HazelcastInstance hazelcast;
@Autowired
private HazelcastSessionRepository repository;
@Test
public void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcast.getMap(
"spring:session:sessions");
assertThat(hazelcastMap.size()).isEqualTo(0);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.size()).isEqualTo(1);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.delete(sessionId);
assertThat(hazelcastMap.size()).isEqualTo(0);
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.hazelcast;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.SocketUtils;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Client.
*
* @author Vedran Pavic
* @author Artem Bilan
* @since 1.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
private static final int PORT = SocketUtils.findAvailableTcpPort();
private static HazelcastInstance hazelcastInstance;
@BeforeClass
public static void setup() {
hazelcastInstance = HazelcastITestUtils.embeddedHazelcastServer(PORT);
}
@AfterClass
public static void teardown() {
if (hazelcastInstance != null) {
hazelcastInstance.shutdown();
}
}
@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcastClient() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig().addAddress("127.0.0.1:" + PORT);
return HazelcastClient.newHazelcastClient(clientConfig);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.hazelcast;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.util.SocketUtils;
/**
* Utility class for Hazelcast integration tests.
*
* @author Vedran Pavic
*/
public final class HazelcastITestUtils {
private HazelcastITestUtils() {
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @param port the port for Hazelcast to bind to
* @return the Hazelcast instance
*/
public static HazelcastInstance embeddedHazelcastServer(int port) {
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
Config config = new Config();
config.getNetworkConfig()
.setPort(port);
config.getMapConfig("spring:session:sessions")
.addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(
HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config);
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @return the Hazelcast instance
*/
public static HazelcastInstance embeddedHazelcastServer() {
return embeddedHazelcastServer(SocketUtils.findAvailableTcpPort());
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Server.
*
* @author Tommy Ludwig
* @author Vedran Pavic
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastServerRepositoryITests extends AbstractHazelcastRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcastServer() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2014-2017 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.hazelcast.config.annotation.web.http;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
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.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.SessionRepository;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.hazelcast.HazelcastITestUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Ensure that the appropriate SessionEvents are fired at the expected times. Additionally
* ensure that the interactions with the {@link SessionRepository} abstraction behave as
* expected after each SessionEvent.
*
* @author Tommy Ludwig
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
private final static int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
@Autowired
private SessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@Before
public void setup() {
this.registry.clear();
}
@Test
public void saveSessionTest() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
S sessionToSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username,
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
Session session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getAttributeNames())
.isEqualTo(sessionToSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(sessionToSave.getAttribute(expectedAttributeName));
}
@Test
public void expiredSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
assertThat(sessionToSave.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.<Session>getSession(sessionToSave.getId())).isNull();
}
@Test
public void deletedSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
this.repository.delete(sessionToSave.getId());
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionDeletedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionDeletedEvent.class);
assertThat(this.repository.getSession(sessionToSave.getId())).isNull();
}
@Test
public void saveUpdatesTimeToLiveTest() throws InterruptedException {
Object lock = new Object();
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
synchronized (lock) {
lock.wait(sessionToSave.getMaxInactiveInterval().minusMillis(500).toMillis());
}
// Get and save the session like SessionRepositoryFilter would.
S sessionToUpdate = this.repository.getSession(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
synchronized (lock) {
lock.wait(sessionToUpdate.getMaxInactiveInterval().minusMillis(100).toMillis());
}
assertThat(this.repository.getSession(sessionToUpdate.getId())).isNotNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcast() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
@Bean
public SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2014-2017 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.hazelcast.config.annotation.web.http;
import java.time.Duration;
import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
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.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test the different configuration options for the {@link EnableHazelcastHttpSession}
* annotation.
*
* @author Tommy Ludwig
*/
public class HazelcastHttpSessionConfigurationXmlTests<S extends Session> {
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public static class CustomXmlMapNameTest<S extends Session> {
@Autowired
private SessionRepository<S> repository;
@Test
public void saveSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
S session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofMinutes(30));
}
@Configuration
@EnableHazelcastHttpSession(sessionMapName = "my-sessions")
static class HazelcastSessionXmlConfigCustomMapName {
@Bean
public HazelcastInstance embeddedHazelcast() {
Config hazelcastConfig = new ClasspathXmlConfig(
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-map-name.xml");
NetworkConfig netConfig = new NetworkConfig();
netConfig.setPort(SocketUtils.findAvailableTcpPort());
hazelcastConfig.setNetworkConfig(netConfig);
return Hazelcast.newHazelcastInstance(hazelcastConfig);
}
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public static class CustomXmlMapNameAndIdleTest<S extends Session> {
@Autowired
private SessionRepository<S> repository;
@Test
public void saveSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
S session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofMinutes(20));
}
@Configuration
@EnableHazelcastHttpSession(sessionMapName = "test-sessions", maxInactiveIntervalInSeconds = 1200)
static class HazelcastSessionXmlConfigCustomMapNameAndIdle {
@Bean
public HazelcastInstance embeddedHazelcast() {
Config hazelcastConfig = new ClasspathXmlConfig(
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-idle-time-map-name.xml");
NetworkConfig netConfig = new NetworkConfig();
netConfig.setPort(SocketUtils.findAvailableTcpPort());
hazelcastConfig.setNetworkConfig(netConfig);
return Hazelcast.newHazelcastInstance(hazelcastConfig);
}
}
}
}

View File

@@ -0,0 +1,581 @@
/*
* Copyright 2014-2017 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.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Optional;
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.jdbc.datasource.DataSourceTransactionManager;
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 org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for {@link JdbcOperationsSessionRepository} integration tests.
*
* @author Vedran Pavic
* @since 1.2.0
*/
@WebAppConfiguration
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public abstract class AbstractJdbcOperationsSessionRepositoryITests {
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.<String>getAttribute(expectedAttributeName))
.isEqualTo(toSave.getAttribute(expectedAttributeName));
this.repository.delete(toSave.getId());
assertThat(this.repository.getSession(toSave.getId())).isNull();
}
@Test
@Transactional(readOnly = true)
public void savesInReadOnlyTransaction() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
.createSession();
this.repository.save(toSave);
}
@Test
public void putAllOnSingleAttrDoesNotRemoveOld() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
.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.<String>getAttribute("a")).isEqualTo(Optional.of("b"));
assertThat(session.<String>getAttribute("1")).isEqualTo(Optional.of("2"));
this.repository.delete(toSave.getId());
}
@Test
public void updateLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository
.createSession();
toSave.setLastAccessedTime(Instant.now().minusSeconds(
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
this.repository.save(toSave);
Instant lastAccessedTime = Instant.now();
toSave.setLastAccessedTime(lastAccessedTime);
this.repository.save(toSave);
Session session = this.repository.getSession(toSave.getId());
assertThat(session).isNotNull();
assertThat(session.isExpired()).isFalse();
assertThat(session.getLastAccessedTime()).isEqualTo(lastAccessedTime);
}
@Test
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
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(Instant.now().minusSeconds(
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS+ 1));
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(Instant.now().minusSeconds(
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
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);
this.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());
}
@Test
public void cleanupInactiveSessionsUsingRepositoryDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
this.repository.save(session);
assertThat(this.repository.getSession(session.getId())).isNotNull();
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNotNull();
Instant now = Instant.now();
session.setLastAccessedTime(now.minus(10, ChronoUnit.MINUTES));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNotNull();
session.setLastAccessedTime(now.minus(30, ChronoUnit.MINUTES));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNull();
}
// gh-580
@Test
public void cleanupInactiveSessionsUsingSessionDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
session.setMaxInactiveInterval(Duration.ofMinutes(45));
this.repository.save(session);
assertThat(this.repository.getSession(session.getId())).isNotNull();
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNotNull();
Instant now = Instant.now();
session.setLastAccessedTime(now.minus(40, ChronoUnit.MINUTES));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNotNull();
session.setLastAccessedTime(now.minus(50, ChronoUnit.MINUTES));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();
assertThat(this.repository.getSession(session.getId())).isNull();
}
private String getSecurityName() {
return this.context.getAuthentication().getName();
}
private String getChangedSecurityName() {
return this.changedContext.getAuthentication().getName();
}
@EnableJdbcHttpSession
protected static class BaseConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using Derby database.
*
* @author Vedran Pavic
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class DerbyJdbcOperationsSessionRepositoryITests
extends AbstractJdbcOperationsSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.DERBY)
.addScript("org/springframework/session/jdbc/schema-derby.sql")
.build();
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using H2 database.
*
* @author Vedran Pavic
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class H2JdbcOperationsSessionRepositoryITests
extends AbstractJdbcOperationsSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("org/springframework/session/jdbc/schema-h2.sql")
.build();
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using HSQLDB database.
*
* @author Vedran Pavic
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class HsqldbJdbcOperationsSessionRepositoryITests
extends AbstractJdbcOperationsSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("org/springframework/session/jdbc/schema-hsqldb.sql")
.build();
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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;
import java.util.Map;
/**
* Extends a basic {@link SessionRepository} to allow finding a session id by the
* principal name. The principal name is defined by the {@link Session} attribute with the
* name {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}.
*
* @param <S> the type of Session being managed by this
* {@link FindByIndexNameSessionRepository}
* @author Rob Winch
*/
public interface FindByIndexNameSessionRepository<S extends Session>
extends SessionRepository<S> {
/**
* <p>
* A common session attribute that contains the current principal name (i.e.
* username).
* </p>
*
* <p>
* It is the responsibility of the developer to ensure the attribute is populated
* since Spring Session is not aware of the authentication mechanism being used.
* </p>
*
* @since 1.1
*/
String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
.concat(".PRINCIPAL_NAME_INDEX_NAME");
/**
* Find a Map of the session id to the {@link Session} of all sessions that contain
* the session attribute with the name
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the value of
* the specified principal 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 a Map (never null) of the session id to the {@link Session} of all sessions
* that contain the session specified index name and the value of the specified index
* name. If no results are found, an empty Map is returned.
*/
Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2014-2017 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;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* <p>
* A {@link Session} implementation that is backed by a {@link java.util.Map}. The
* defaults for the properties are:
* </p>
* <ul>
* <li>id - a secure random generated id</li>
* <li>creationTime - the moment the {@link MapSession} was instantiated</li>
* <li>lastAccessedTime - the moment the {@link MapSession} was instantiated</li>
* <li>maxInactiveInterval - 30 minutes</li>
* </ul>
*
* <p>
* This implementation has no synchronization, so it is best to use the copy constructor
* when working on multiple threads.
* </p>
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
*/
public final class MapSession implements Session, Serializable {
/**
* Default {@link #setMaxInactiveInterval(Duration)} (30 minutes).
*/
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
private String id;
private Map<String, Object> sessionAttrs = new HashMap<>();
private Instant creationTime = Instant.now();
private Instant lastAccessedTime = this.creationTime;
/**
* Defaults to 30 minutes.
*/
private Duration maxInactiveInterval = Duration.ofSeconds(DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
/**
* Creates a new instance with a secure randomly generated identifier.
*/
public MapSession() {
this(UUID.randomUUID().toString());
}
/**
* Creates a new instance with the specified id. This is preferred to the default
* constructor when the id is known to prevent unnecessary consumption on entropy
* which can be slow.
*
* @param id the identifier to use
*/
public MapSession(String id) {
this.id = id;
}
/**
* Creates a new instance from the provided {@link Session}.
*
* @param session the {@link Session} to initialize this {@link Session} with. Cannot
* be null.
*/
public MapSession(Session session) {
if (session == null) {
throw new IllegalArgumentException("session cannot be null");
}
this.id = session.getId();
this.sessionAttrs = new HashMap<>(
session.getAttributeNames().size());
for (String attrName : session.getAttributeNames()) {
session.getAttribute(attrName)
.ifPresent(attrValue -> this.sessionAttrs.put(attrName, attrValue));
}
this.lastAccessedTime = session.getLastAccessedTime();
this.creationTime = session.getCreationTime();
this.maxInactiveInterval = session.getMaxInactiveInterval();
}
public void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}
public Instant getCreationTime() {
return this.creationTime;
}
public String getId() {
return this.id;
}
public Instant getLastAccessedTime() {
return this.lastAccessedTime;
}
public void setMaxInactiveInterval(Duration interval) {
this.maxInactiveInterval = interval;
}
public Duration getMaxInactiveInterval() {
return this.maxInactiveInterval;
}
public boolean isExpired() {
return isExpired(Instant.now());
}
boolean isExpired(Instant now) {
if (this.maxInactiveInterval.isNegative()) {
return false;
}
return now.minus(this.maxInactiveInterval).compareTo(this.lastAccessedTime) >= 0;
}
@SuppressWarnings("unchecked")
public <T> Optional<T> getAttribute(String attributeName) {
return Optional.ofNullable((T) this.sessionAttrs.get(attributeName));
}
public Set<String> getAttributeNames() {
return this.sessionAttrs.keySet();
}
public void setAttribute(String attributeName, Object attributeValue) {
if (attributeValue == null) {
removeAttribute(attributeName);
}
else {
this.sessionAttrs.put(attributeName, attributeValue);
}
}
public void removeAttribute(String attributeName) {
this.sessionAttrs.remove(attributeName);
}
/**
* Sets the time that this {@link Session} was created. The default is when the
* {@link Session} was instantiated.
* @param creationTime the time that this {@link Session} was created.
*/
public void setCreationTime(Instant creationTime) {
this.creationTime = creationTime;
}
/**
* Sets the identifier for this {@link Session}. The id should be a secure random
* generated value to prevent malicious users from guessing this value. The default is
* a secure random generated identifier.
*
* @param id the identifier for this session.
*/
public void setId(String id) {
this.id = id;
}
public boolean equals(Object obj) {
return obj instanceof Session && this.id.equals(((Session) obj).getId());
}
public int hashCode() {
return this.id.hashCode();
}
private static final long serialVersionUID = 7160779239673823561L;
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2014-2017 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;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
/**
* A {@link SessionRepository} backed by a {@link java.util.Map} and that uses a
* {@link MapSession}. By default a {@link java.util.concurrent.ConcurrentHashMap} is
* used, but a custom {@link java.util.Map} can be injected to use distributed maps
* provided by NoSQL stores like Redis and Hazelcast.
*
* <p>
* The implementation does NOT support firing {@link SessionDeletedEvent} or
* {@link SessionExpiredEvent}.
* </p>
*
* @author Rob Winch
* @since 1.0
*/
public class MapSessionRepository implements SessionRepository<Session> {
/**
* If non-null, this value is used to override
* {@link Session#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private final Map<String, Session> sessions;
/**
* Creates an instance backed by a {@link java.util.concurrent.ConcurrentHashMap}.
*/
public MapSessionRepository() {
this(new ConcurrentHashMap<>());
}
/**
* Creates a new instance backed by the provided {@link java.util.Map}. This allows
* injecting a distributed {@link java.util.Map}.
*
* @param sessions the {@link java.util.Map} to use. Cannot be null.
*/
public MapSessionRepository(Map<String, Session> sessions) {
if (sessions == null) {
throw new IllegalArgumentException("sessions cannot be null");
}
this.sessions = sessions;
}
/**
* If non-null, this value is used to override
* {@link Session#setMaxInactiveInterval(Duration)}.
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = Integer.valueOf(defaultMaxInactiveInterval);
}
public void save(Session session) {
this.sessions.put(session.getId(), new MapSession(session));
}
public Session getSession(String id) {
Session saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
delete(saved.getId());
return null;
}
return new MapSession(saved);
}
public void delete(String id) {
this.sessions.remove(id);
}
public Session createSession() {
Session result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2014-2017 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;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
/**
* Provides a way to identify a user in an agnostic way. This allows the session to be
* used by an HttpSession, WebSocket Session, or even non web related sessions.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
*/
public interface Session {
/**
* Gets a unique string that identifies the {@link Session}.
*
* @return a unique string that identifies the {@link Session}
*/
String getId();
/**
* Gets the Object associated with the specified name or null if no Object is
* associated to that name.
*
* @param attributeName the name of the attribute to get
* @return the Object associated with the specified name or null if no Object is
* associated to that name
* @param <T> The return type of the attribute
*/
<T> Optional<T> getAttribute(String attributeName);
/**
* Gets the attribute names that have a value associated with it. Each value can be
* passed into {@link org.springframework.session.Session#getAttribute(String)} to
* obtain the attribute value.
*
* @return the attribute names that have a value associated with it.
* @see #getAttribute(String)
*/
Set<String> getAttributeNames();
/**
* Sets the attribute value for the provided attribute name. If the attributeValue is
* null, it has the same result as removing the attribute with
* {@link org.springframework.session.Session#removeAttribute(String)} .
*
* @param attributeName the attribute name to set
* @param attributeValue the value of the attribute to set. If null, the attribute
* will be removed.
*/
void setAttribute(String attributeName, Object attributeValue);
/**
* Removes the attribute with the provided attribute name.
* @param attributeName the name of the attribute to remove
*/
void removeAttribute(String attributeName);
/**
* Gets the time when this session was created.
*
* @return the time when this session was created.
*/
Instant getCreationTime();
/**
* Sets the last accessed time.
*
* @param lastAccessedTime the last accessed time
*/
void setLastAccessedTime(Instant lastAccessedTime);
/**
* Gets the last time this {@link Session} was accessed.
*
* @return the last time the client sent a request associated with the session
*/
Instant getLastAccessedTime();
/**
* Sets the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*
* @param interval the amount of time that the {@link Session} should be kept alive
* between client requests.
*/
void setMaxInactiveInterval(Duration interval);
/**
* Gets the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*
* @return the maximum inactive interval between requests before this session will be
* invalidated. A negative time indicates that the session will never timeout.
*/
Duration getMaxInactiveInterval();
/**
* Returns true if the session is expired.
*
* @return true if the session is expired, else false.
*/
boolean isExpired();
}

View File

@@ -0,0 +1,73 @@
/*
* 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;
/**
* A repository interface for managing {@link Session} instances.
*
* @param <S> the {@link Session} type
* @author Rob Winch
* @since 1.0
*/
public interface SessionRepository<S extends Session> {
/**
* Creates a new {@link Session} that is capable of being persisted by this
* {@link SessionRepository}.
*
* <p>
* This allows optimizations and customizations in how the {@link Session} is
* persisted. For example, the implementation returned might keep track of the changes
* ensuring that only the delta needs to be persisted on a save.
* </p>
*
* @return a new {@link Session} that is capable of being persisted by this
* {@link SessionRepository}
*/
S createSession();
/**
* Ensures the {@link Session} created by
* {@link org.springframework.session.SessionRepository#createSession()} is saved.
*
* <p>
* Some implementations may choose to save as the {@link Session} is updated by
* returning a {@link Session} that immediately persists any changes. In this case,
* this method may not actually do anything.
* </p>
*
* @param session the {@link Session} to save
*/
void save(S session);
/**
* Gets the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*
* @param id the {@link org.springframework.session.Session#getId()} to lookup
* @return the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*/
S getSession(String id);
/**
* Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
* if the {@link Session} is not found.
* @param id the {@link org.springframework.session.Session#getId()} to delete
*/
void delete(String id);
}

View File

@@ -0,0 +1,79 @@
/*
* 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.config.annotation.web.http;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
/**
* Add this annotation to an {@code @Configuration} class to expose the
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and backed by a
* user provided implementation of {@link SessionRepository}. In order to leverage the
* annotation, a single {@link SessionRepository} bean must be provided. For example:
*
* <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableSpringHttpSession}
* public class SpringHttpSessionConfig {
*
* {@literal @Bean}
* public MapSessionRepository sessionRepository() {
* return new MapSessionRepository();
* }
*
* }
* </code> </pre>
*
* <p>
* It is important to note that no infrastructure for session expirations is configured
* for you out of the box. This is because things like session expiration are highly
* implementation dependent. This means if you require cleaning up expired sessions, you
* are responsible for cleaning up the expired sessions.
* </p>
*
* <p>
* The following is provided for you with the base configuration:
* </p>
*
* <ul>
* <li>SessionRepositoryFilter - is responsible for wrapping the HttpServletRequest with
* an implementation of HttpSession that is backed by a SessionRepository</li>
* <li>SessionEventHttpSessionListenerAdapter - is responsible for translating Spring
* Session events into HttpSessionEvent. In order for it to work, the implementation of
* SessionRepository you provide must support {@link SessionCreatedEvent} and
* {@link SessionDestroyedEvent}.</li>
* <li>
* </ul>
*
* @author Rob Winch
* @since 1.1
*/
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringHttpSessionConfiguration.class)
@Configuration
public @interface EnableSpringHttpSession {
}

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2014-2017 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.config.annotation.web.http;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import javax.servlet.SessionCookieConfig;
import javax.servlet.http.HttpSessionListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.MultiHttpSessionStrategy;
import org.springframework.session.web.http.SessionEventHttpSessionListenerAdapter;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Configures the basics for setting up Spring Session in a web environment. In order to
* use it, you must provide a {@link SessionRepository}. For example:
*
* <pre>
* {@literal @Configuration}
* {@literal @EnableSpringHttpSession}
* public class SpringHttpSessionConfig {
*
* {@literal @Bean}
* public MapSessionRepository sessionRepository() {
* return new MapSessionRepository();
* }
*
* }
* </pre>
*
* <p>
* It is important to note that no infrastructure for session expirations is configured
* for you out of the box. This is because things like session expiration are highly
* implementation dependent. This means if you require cleaning up expired sessions, you
* are responsible for cleaning up the expired sessions.
* </p>
*
* <p>
* The following is provided for you with the base configuration:
* </p>
*
* <ul>
* <li>SessionRepositoryFilter - is responsible for wrapping the HttpServletRequest with
* an implementation of HttpSession that is backed by a SessionRepository</li>
* <li>SessionEventHttpSessionListenerAdapter - is responsible for translating Spring
* Session events into HttpSessionEvent. In order for it to work, the implementation of
* SessionRepository you provide must support {@link SessionCreatedEvent} and
* {@link SessionDestroyedEvent}.</li>
* <li>
* </ul>
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.1
*
* @see EnableSpringHttpSession
*/
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private final Log logger = LogFactory.getLog(getClass());
private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();
private boolean usesSpringSessionRememberMeServices;
private ServletContext servletContext;
private CookieSerializer cookieSerializer;
private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;
private List<HttpSessionListener> httpSessionListeners = new ArrayList<>();
@PostConstruct
public void init() {
CookieSerializer cookieSerializer = this.cookieSerializer != null
? this.cookieSerializer : createDefaultCookieSerializer();
this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);
}
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
sessionRepositoryFilter.setHttpSessionStrategy(
(MultiHttpSessionStrategy) this.httpSessionStrategy);
}
else {
sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
}
return sessionRepositoryFilter;
}
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
if (ClassUtils.isPresent(
"org.springframework.security.web.authentication.RememberMeServices",
null)) {
this.usesSpringSessionRememberMeServices = !ObjectUtils
.isEmpty(applicationContext
.getBeanNamesForType(SpringSessionRememberMeServices.class));
}
}
@Autowired(required = false)
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.cookieSerializer = cookieSerializer;
}
@Autowired(required = false)
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
this.httpSessionStrategy = httpSessionStrategy;
}
@Autowired(required = false)
public void setHttpSessionListeners(List<HttpSessionListener> listeners) {
this.httpSessionListeners = listeners;
}
private CookieSerializer createDefaultCookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
if (this.servletContext != null) {
SessionCookieConfig sessionCookieConfig = null;
try {
sessionCookieConfig = this.servletContext.getSessionCookieConfig();
}
catch (UnsupportedOperationException e) {
this.logger
.warn("Unable to obtain SessionCookieConfig: " + e.getMessage());
}
if (sessionCookieConfig != null) {
if (sessionCookieConfig.getName() != null) {
cookieSerializer.setCookieName(sessionCookieConfig.getName());
}
if (sessionCookieConfig.getDomain() != null) {
cookieSerializer.setDomainName(sessionCookieConfig.getDomain());
}
if (sessionCookieConfig.getPath() != null) {
cookieSerializer.setCookiePath(sessionCookieConfig.getPath());
}
if (sessionCookieConfig.getMaxAge() != -1) {
cookieSerializer.setCookieMaxAge(sessionCookieConfig.getMaxAge());
}
}
}
if (this.usesSpringSessionRememberMeServices) {
cookieSerializer.setRememberMeRequestAttribute(
SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
}
return cookieSerializer;
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.events;
import org.springframework.context.ApplicationEvent;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* For {@link SessionRepository} implementations that support it, this event is fired when
* a {@link Session} is updated.
*
* @author Rob Winch
* @since 1.1
*/
@SuppressWarnings("serial")
public abstract class AbstractSessionEvent extends ApplicationEvent {
private final String sessionId;
private final Session session;
protected AbstractSessionEvent(Object source, String sessionId) {
super(source);
this.sessionId = sessionId;
this.session = null;
}
AbstractSessionEvent(Object source, Session session) {
super(source);
this.session = session;
this.sessionId = session.getId();
}
/**
* Gets the {@link Session} that was destroyed. For some {@link SessionRepository}
* implementations it may not be possible to get the original session in which case
* this may be null.
*
* @param <S> The type of Session
* @return the expired {@link Session} or null if the data store does not support
* obtaining it
*/
@SuppressWarnings("unchecked")
public <S extends Session> S getSession() {
return (S) this.session;
}
public String getSessionId() {
return this.sessionId;
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.events;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* For {@link SessionRepository} implementations that support it, this event is fired when
* a {@link Session} is created.
*
* @author Rob Winch
* @since 1.0
*
*/
@SuppressWarnings("serial")
public class SessionCreatedEvent extends AbstractSessionEvent {
public SessionCreatedEvent(Object source, String sessionId) {
super(source, sessionId);
}
/**
* Create a new {@link SessionCreatedEvent}.
* @param source The Source of the SessionCreatedEvent
* @param session the Session that was created
*/
public SessionCreatedEvent(Object source, Session session) {
super(source, session);
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.events;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* For {@link SessionRepository} implementations that support it, this event is fired when
* a {@link Session} is destroyed via deletion.
*
* @author Mark Anderson
* @author Rob Winch
* @since 1.1
*
*/
@SuppressWarnings("serial")
public class SessionDeletedEvent extends SessionDestroyedEvent {
public SessionDeletedEvent(Object source, String sessionId) {
super(source, sessionId);
}
public SessionDeletedEvent(Object source, Session session) {
super(source, session);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.events;
import org.springframework.session.Session;
/**
* Base class for events fired when a {@link Session} is destroyed explicitly.
*
* @author Rob Winch
* @since 1.0
*
*/
@SuppressWarnings("serial")
public class SessionDestroyedEvent extends AbstractSessionEvent {
public SessionDestroyedEvent(Object source, String sessionId) {
super(source, sessionId);
}
/**
* Create a new {@link SessionDestroyedEvent}.
* @param source The Source of the SessionDestoryedEvent
* @param session the Session that was created
*/
public SessionDestroyedEvent(Object source, Session session) {
super(source, session);
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.events;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* For {@link SessionRepository} implementations that support it, this event is fired when
* a {@link Session} is destroyed via expiration.
*
* @author Mark Anderson
* @author Rob Winch
* @since 1.1
*
*/
@SuppressWarnings("serial")
public class SessionExpiredEvent extends SessionDestroyedEvent {
public SessionExpiredEvent(Object source, String sessionId) {
super(source, sessionId);
}
public SessionExpiredEvent(Object source, Session session) {
super(source, session);
}
}

View File

@@ -0,0 +1,748 @@
/*
* Copyright 2014-2017 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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
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();
*
* // ... configure jdbcTemplate ...
*
* PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
*
* // ... configure transactionManager ...
*
* JdbcOperationsSessionRepository sessionRepository =
* new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
* </pre>
*
* For additional information on how to create and configure {@link JdbcTemplate} and
* {@link PlatformTransactionManager}, refer to the
* <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
* Spring Framework Reference Documentation</a>.
* <p>
* By default, this implementation uses <code>SPRING_SESSION</code> and
* <code>SPRING_SESSION_ATTRIBUTES</code> tables to store sessions. Note that the table
* name can be customized using the {@link #setTableName(String)} method. In that case the
* table used to store attributes will be named using the provided table name, suffixed
* with <code>_ATTRIBUTES</code>.
*
* Depending on your database, the table definition can be described as below:
*
* <pre class="code">
* CREATE TABLE SPRING_SESSION (
* SESSION_ID CHAR(36),
* CREATION_TIME BIGINT NOT NULL,
* LAST_ACCESS_TIME BIGINT NOT NULL,
* MAX_INACTIVE_INTERVAL INT NOT NULL,
* PRINCIPAL_NAME VARCHAR(100),
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
* );
*
* CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
*
* CREATE TABLE SPRING_SESSION_ATTRIBUTES (
* SESSION_ID CHAR(36),
* ATTRIBUTE_NAME VARCHAR(200),
* ATTRIBUTE_BYTES BYTEA,
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
* );
*
* CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (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> {
/**
* The default name of database table used by Spring Session to store sessions.
*/
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String CREATE_SESSION_QUERY =
"INSERT INTO %TABLE_NAME%(SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, PRINCIPAL_NAME) " +
"VALUES (?, ?, ?, ?, ?)";
private static final String CREATE_SESSION_ATTRIBUTE_QUERY =
"INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " +
"VALUES (?, ?, ?)";
private static final String GET_SESSION_QUERY =
"SELECT S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
"FROM %TABLE_NAME% S " +
"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.SESSION_ID = SA.SESSION_ID " +
"WHERE S.SESSION_ID = ?";
private static final String UPDATE_SESSION_QUERY =
"UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, PRINCIPAL_NAME = ? " +
"WHERE SESSION_ID = ?";
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY =
"UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? " +
"WHERE SESSION_ID = ? " +
"AND ATTRIBUTE_NAME = ?";
private static final String DELETE_SESSION_ATTRIBUTE_QUERY =
"DELETE FROM %TABLE_NAME%_ATTRIBUTES " +
"WHERE SESSION_ID = ? " +
"AND ATTRIBUTE_NAME = ?";
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 S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
"FROM %TABLE_NAME% S " +
"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.SESSION_ID = SA.SESSION_ID " +
"WHERE S.PRINCIPAL_NAME = ?";
private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY =
"DELETE FROM %TABLE_NAME% " +
"WHERE MAX_INACTIVE_INTERVAL < (? - LAST_ACCESS_TIME) / 1000";
private static final Log logger = LogFactory
.getLog(JdbcOperationsSessionRepository.class);
private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
private final JdbcOperations jdbcOperations;
private final TransactionOperations transactionOperations;
private final ResultSetExtractor<List<Session>> extractor = new SessionResultSetExtractor();
/**
* The name of database table used by Spring Session to store sessions.
*/
private String tableName = DEFAULT_TABLE_NAME;
private String createSessionQuery;
private String createSessionAttributeQuery;
private String getSessionQuery;
private String updateSessionQuery;
private String updateSessionAttributeQuery;
private String deleteSessionAttributeQuery;
private String deleteSessionQuery;
private String listSessionsByPrincipalNameQuery;
private String deleteSessionsByLastAccessTimeQuery;
/**
* If non-null, this value is used to override the default value for
* {@link JdbcSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private ConversionService conversionService;
private LobHandler lobHandler = new DefaultLobHandler();
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* default {@link JdbcOperations} to manage sessions.
* @param dataSource the {@link DataSource} to use
* @param transactionManager the {@link PlatformTransactionManager} to use
*/
public JdbcOperationsSessionRepository(DataSource dataSource,
PlatformTransactionManager transactionManager) {
this(createDefaultJdbcTemplate(dataSource), transactionManager);
}
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided {@link JdbcOperations} to manage sessions.
* @param jdbcOperations the {@link JdbcOperations} to use
* @param transactionManager the {@link PlatformTransactionManager} to use
*/
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
PlatformTransactionManager transactionManager) {
Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
this.jdbcOperations = jdbcOperations;
this.transactionOperations = createTransactionTemplate(transactionManager);
this.conversionService = createDefaultConversionService();
prepareQueries();
}
/**
* 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();
prepareQueries();
}
/**
* Set the custom SQL query used to create the session.
* @param createSessionQuery the SQL query string
*/
public void setCreateSessionQuery(String createSessionQuery) {
Assert.hasText(createSessionQuery, "Query must not be empty");
this.createSessionQuery = createSessionQuery;
}
/**
* Set the custom SQL query used to create the session attribute.
* @param createSessionAttributeQuery the SQL query string
*/
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
this.createSessionAttributeQuery = createSessionAttributeQuery;
}
/**
* Set the custom SQL query used to retrieve the session.
* @param getSessionQuery the SQL query string
*/
public void setGetSessionQuery(String getSessionQuery) {
Assert.hasText(getSessionQuery, "Query must not be empty");
this.getSessionQuery = getSessionQuery;
}
/**
* Set the custom SQL query used to update the session.
* @param updateSessionQuery the SQL query string
*/
public void setUpdateSessionQuery(String updateSessionQuery) {
Assert.hasText(updateSessionQuery, "Query must not be empty");
this.updateSessionQuery = updateSessionQuery;
}
/**
* Set the custom SQL query used to update the session attribute.
* @param updateSessionAttributeQuery the SQL query string
*/
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session attribute.
* @param deleteSessionAttributeQuery the SQL query string
*/
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session.
* @param deleteSessionQuery the SQL query string
*/
public void setDeleteSessionQuery(String deleteSessionQuery) {
Assert.hasText(deleteSessionQuery, "Query must not be empty");
this.deleteSessionQuery = deleteSessionQuery;
}
/**
* Set the custom SQL query used to retrieve the sessions by principal name.
* @param listSessionsByPrincipalNameQuery the SQL query string
*/
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
}
/**
* Set the custom SQL query used to delete the sessions by last access time.
* @param deleteSessionsByLastAccessTimeQuery the SQL query string
*/
public void setDeleteSessionsByLastAccessTimeQuery(String deleteSessionsByLastAccessTimeQuery) {
Assert.hasText(deleteSessionsByLastAccessTimeQuery, "Query must not be empty");
this.deleteSessionsByLastAccessTimeQuery = deleteSessionsByLastAccessTimeQuery;
}
/**
* 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;
}
/**
* Sets the {@link ConversionService} to use.
* @param conversionService the converter to set
*/
public void setConversionService(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = conversionService;
}
public JdbcSession createSession() {
JdbcSession session = new JdbcSession();
if (this.defaultMaxInactiveInterval != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return session;
}
public void save(final JdbcSession session) {
if (session.isNew()) {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.createSessionQuery,
ps -> {
ps.setString(1, session.getId());
ps.setLong(2, session.getCreationTime().toEpochMilli());
ps.setLong(3, session.getLastAccessedTime().toEpochMilli());
ps.setInt(4, (int) session.getMaxInactiveInterval().getSeconds());
ps.setString(5, session.getPrincipalName());
});
if (!session.getAttributeNames().isEmpty()) {
final List<String> attributeNames = new ArrayList<>(session.getAttributeNames());
JdbcOperationsSessionRepository.this.jdbcOperations.batchUpdate(
JdbcOperationsSessionRepository.this.createSessionAttributeQuery,
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, session.getId());
ps.setString(2, attributeName);
serialize(ps, 3, session.getAttribute(attributeName).orElse(null));
}
public int getBatchSize() {
return attributeNames.size();
}
});
}
}
});
}
else {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (session.isChanged()) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.updateSessionQuery,
ps -> {
ps.setLong(1, session.getLastAccessedTime().toEpochMilli());
ps.setInt(2, (int) session.getMaxInactiveInterval().getSeconds());
ps.setString(3, session.getPrincipalName());
ps.setString(4, session.getId());
});
}
Map<String, Object> delta = session.getDelta();
if (!delta.isEmpty()) {
for (final Map.Entry<String, Object> entry : delta.entrySet()) {
if (entry.getValue() == null) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.deleteSessionAttributeQuery,
ps -> {
ps.setString(1, session.getId());
ps.setString(2, entry.getKey());
});
}
else {
int updatedCount = JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.updateSessionAttributeQuery,
ps -> {
serialize(ps, 1, entry.getValue());
ps.setString(2, session.getId());
ps.setString(3, entry.getKey());
});
if (updatedCount == 0) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.createSessionAttributeQuery,
ps -> {
ps.setString(1, session.getId());
ps.setString(2, entry.getKey());
serialize(ps, 3, entry.getValue());
});
}
}
}
}
}
});
}
session.clearChangeFlags();
}
public JdbcSession getSession(final String id) {
final Session session = this.transactionOperations.execute(status -> {
List<Session> sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query(
JdbcOperationsSessionRepository.this.getSessionQuery,
ps -> ps.setString(1, id),
JdbcOperationsSessionRepository.this.extractor
);
if (sessions.isEmpty()) {
return null;
}
return sessions.get(0);
});
if (session != null) {
if (session.isExpired()) {
delete(id);
}
else {
return new JdbcSession(session);
}
}
return null;
}
public void delete(final String id) {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.deleteSessionQuery, id);
}
});
}
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName,
final String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<Session> sessions = this.transactionOperations.execute(status ->
JdbcOperationsSessionRepository.this.jdbcOperations.query(
JdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
ps -> ps.setString(1, indexValue),
JdbcOperationsSessionRepository.this.extractor));
Map<String, JdbcSession> sessionMap = new HashMap<>(
sessions.size());
for (Session session : sessions) {
sessionMap.put(session.getId(), new JdbcSession(session));
}
return sessionMap;
}
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanUpExpiredSessions() {
int deletedCount = this.transactionOperations.execute(transactionStatus ->
JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.deleteSessionsByLastAccessTimeQuery,
System.currentTimeMillis()));
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
private static JdbcTemplate createDefaultJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.afterPropertiesSet();
return jdbcTemplate;
}
private static TransactionTemplate createTransactionTemplate(
PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(
transactionManager);
transactionTemplate.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.afterPropertiesSet();
return transactionTemplate;
}
private static GenericConversionService createDefaultConversionService() {
GenericConversionService converter = new GenericConversionService();
converter.addConverter(Object.class, byte[].class,
new SerializingConverter());
converter.addConverter(byte[].class, Object.class,
new DeserializingConverter());
return converter;
}
private String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private void prepareQueries() {
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
this.listSessionsByPrincipalNameQuery =
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
this.deleteSessionsByLastAccessTimeQuery =
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY);
}
private void serialize(PreparedStatement ps, int paramIndex, Object attributeValue)
throws SQLException {
this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex,
(byte[]) this.conversionService.convert(attributeValue,
TypeDescriptor.valueOf(Object.class),
TypeDescriptor.valueOf(byte[].class)));
}
private Object deserialize(ResultSet rs, String columnName)
throws SQLException {
return this.conversionService.convert(
this.lobHandler.getBlobAsBytes(rs, columnName),
TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(Object.class));
}
/**
* The {@link Session} to use for {@link JdbcOperationsSessionRepository}.
*
* @author Vedran Pavic
*/
final class JdbcSession implements Session {
private final Session delegate;
private boolean isNew;
private boolean changed;
private Map<String, Object> delta = new HashMap<>();
JdbcSession() {
this.delegate = new MapSession();
this.isNew = true;
}
JdbcSession(Session delegate) {
Assert.notNull(delegate, "Session cannot be null");
this.delegate = delegate;
}
boolean isNew() {
return this.isNew;
}
boolean isChanged() {
return this.changed;
}
Map<String, Object> getDelta() {
return this.delta;
}
void clearChangeFlags() {
this.isNew = false;
this.changed = false;
this.delta.clear();
}
String getPrincipalName() {
return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
}
public String getId() {
return this.delegate.getId();
}
public <T> Optional<T> getAttribute(String attributeName) {
return this.delegate.getAttribute(attributeName);
}
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.delta.put(attributeName, attributeValue);
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) ||
SPRING_SECURITY_CONTEXT.equals(attributeName)) {
this.changed = true;
}
}
public void removeAttribute(String attributeName) {
this.delegate.removeAttribute(attributeName);
this.delta.put(attributeName, null);
}
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
}
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
}
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
public boolean isExpired() {
return this.delegate.isExpired();
}
}
/**
* Resolves the Spring Security principal name.
*
* @author Vedran Pavic
*/
static class PrincipalNameResolver {
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
Optional<String> principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName.isPresent()) {
return principalName.get();
}
Optional<Object> authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication.isPresent()) {
Expression expression = this.parser
.parseExpression("authentication?.name");
return expression.getValue(authentication.get(), String.class);
}
return null;
}
}
private class SessionResultSetExtractor implements ResultSetExtractor<List<Session>> {
public List<Session> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<Session> sessions = new ArrayList<>();
while (rs.next()) {
String id = rs.getString("SESSION_ID");
MapSession session;
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
session = (MapSession) getLast(sessions);
}
else {
session = new MapSession(id);
session.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
session.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
session.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
}
String attributeName = rs.getString("ATTRIBUTE_NAME");
if (attributeName != null) {
session.setAttribute(attributeName, deserialize(rs, "ATTRIBUTE_BYTES"));
}
sessions.add(session);
}
return sessions;
}
private Session getLast(List<Session> sessions) {
return sessions.get(sessions.size() - 1);
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
/**
* 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.
*
* For additional information on how to configure data access related concerns, please
* refer to the
* <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
* Spring Framework Reference Documentation</a>.
*
* @author Vedran Pavic
* @since 1.2.0
* @see EnableSpringHttpSession
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(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 JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
/**
* 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 MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
}

View File

@@ -0,0 +1,195 @@
/*
* 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.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
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.transaction.PlatformTransactionManager;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* 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
* @author Eddú Meléndez
* @since 1.2.0
* @see EnableJdbcHttpSession
*/
@Configuration
@EnableScheduling
public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, ImportAware, EmbeddedValueResolverAware {
private String tableName;
private Integer maxInactiveIntervalInSeconds;
private LobHandler lobHandler;
@Autowired(required = false)
@Qualifier("conversionService")
private ConversionService conversionService;
private ConversionService springSessionConversionService;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public JdbcTemplate springSessionJdbcOperations(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcOperationsSessionRepository sessionRepository(
@Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations,
PlatformTransactionManager transactionManager) {
JdbcOperationsSessionRepository sessionRepository =
new JdbcOperationsSessionRepository(jdbcOperations, transactionManager);
String tableName = getTableName();
if (StringUtils.hasText(tableName)) {
sessionRepository.setTableName(tableName);
}
sessionRepository
.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
}
if (this.springSessionConversionService != null) {
sessionRepository.setConversionService(this.springSessionConversionService);
}
else if (this.conversionService != null) {
sessionRepository.setConversionService(this.conversionService);
}
else if (deserializingConverterSupportsCustomClassLoader()) {
GenericConversionService conversionService = createConversionServiceWithBeanClassLoader();
sessionRepository.setConversionService(conversionService);
}
return sessionRepository;
}
/**
* This must be a separate method because some ClassLoaders load the entire method
* definition even if an if statement guards against it loading. This means that older
* versions of Spring would cause a NoSuchMethodError if this were defined in
* {@link #sessionRepository(JdbcOperations, PlatformTransactionManager)}.
*
* @return the default {@link ConversionService}
*/
private GenericConversionService createConversionServiceWithBeanClassLoader() {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(Object.class, byte[].class,
new SerializingConverter());
conversionService.addConverter(byte[].class, Object.class,
new DeserializingConverter(this.classLoader));
return conversionService;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Autowired(required = false)
@Qualifier("springSessionLobHandler")
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
@Autowired(required = false)
@Qualifier("springSessionConversionService")
public void setSpringSessionConversionService(ConversionService conversionService) {
this.springSessionConversionService = conversionService;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
private String getTableName() {
String systemProperty = System.getProperty("spring.session.jdbc.tableName", "");
if (StringUtils.hasText(systemProperty)) {
return systemProperty;
}
return this.tableName;
}
private boolean deserializingConverterSupportsCustomClassLoader() {
return ClassUtils.hasConstructor(DeserializingConverter.class, ClassLoader.class);
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata
.getAnnotationAttributes(EnableJdbcHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
String tableNameValue = enableAttrs.getString("tableName");
if (StringUtils.hasText(tableNameValue)) {
this.tableName = this.embeddedValueResolver
.resolveStringValue(tableNameValue);
}
this.maxInactiveIntervalInSeconds = enableAttrs
.getNumber("maxInactiveIntervalInSeconds");
}
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
/**
* Property placeholder to process the @Scheduled annotation.
* @return the {@link PropertySourcesPlaceholderConfigurer} to use
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2014-2017 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.security;
import java.util.Date;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* Ensures that calling {@link #expireNow()} propagates to Spring Session, since this
* session information contains only derived data and is not the authoritative source.
*
* @param <S> the {@link Session} type.
* @author Joris Kuipers
* @author Vedran Pavic
* @since 1.3
*/
class SpringSessionBackedSessionInformation<S extends Session>
extends SessionInformation {
static final String EXPIRED_ATTR = SpringSessionBackedSessionInformation.class
.getName() + ".EXPIRED";
private static final Log logger = LogFactory
.getLog(SpringSessionBackedSessionInformation.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private final SessionRepository<S> sessionRepository;
SpringSessionBackedSessionInformation(S session,
SessionRepository<S> sessionRepository) {
super(resolvePrincipal(session), session.getId(),
Date.from(session.getLastAccessedTime()));
this.sessionRepository = sessionRepository;
session.getAttribute(EXPIRED_ATTR).ifPresent(expired -> {
if (Boolean.TRUE.equals(expired)) {
super.expireNow();
}
});
}
/**
* Tries to determine the principal's name from the given Session.
*
* @param session Spring Session session
* @return the principal's name, or empty String if it couldn't be determined
*/
private static String resolvePrincipal(Session session) {
Optional<String> principalName = session
.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if (principalName.isPresent()) {
return principalName.get();
}
Optional<SecurityContext> securityContext = session
.getAttribute(SPRING_SECURITY_CONTEXT);
if (securityContext.isPresent()
&& securityContext.get().getAuthentication() != null) {
return securityContext.get().getAuthentication().getName();
}
return "";
}
@Override
public void expireNow() {
if (logger.isDebugEnabled()) {
logger.debug("Expiring session " + getSessionId() + " for user '"
+ getPrincipal() + "', presumably because maximum allowed concurrent "
+ "sessions was exceeded");
}
super.expireNow();
S session = this.sessionRepository.getSession(getSessionId());
if (session != null) {
session.setAttribute(EXPIRED_ATTR, Boolean.TRUE);
this.sessionRepository.save(session);
}
else {
logger.info("Could not find Session with id " + getSessionId()
+ " to mark as expired");
}
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2014-2017 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.security;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link SessionRegistry} that retrieves session information from Spring Session,
* rather than maintaining it itself. This allows concurrent session management with
* Spring Security in a clustered environment.
* <p>
* Relies on being able to derive the same String-based representation of the principal
* given to {@link #getAllSessions(Object, boolean)} as used by Spring Session in order to
* look up the user's sessions.
* <p>
* Does not support {@link #getAllPrincipals()}, since that information is not available.
*
* @param <S> the {@link Session} type.
* @author Joris Kuipers
* @author Vedran Pavic
* @since 1.3
*/
public class SpringSessionBackedSessionRegistry<S extends Session>
implements SessionRegistry {
private final FindByIndexNameSessionRepository<S> sessionRepository;
public SpringSessionBackedSessionRegistry(
FindByIndexNameSessionRepository<S> sessionRepository) {
Assert.notNull(sessionRepository, "sessionRepository cannot be null");
this.sessionRepository = sessionRepository;
}
public List<Object> getAllPrincipals() {
throw new UnsupportedOperationException("SpringSessionBackedSessionRegistry does "
+ "not support retrieving all principals, since Spring Session provides "
+ "no way to obtain that information");
}
public List<SessionInformation> getAllSessions(Object principal,
boolean includeExpiredSessions) {
Collection<S> sessions = this.sessionRepository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
name(principal)).values();
List<SessionInformation> infos = new ArrayList<>();
for (S session : sessions) {
if (includeExpiredSessions || !Boolean.TRUE.equals(session
.getAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR).orElse(false))) {
infos.add(new SpringSessionBackedSessionInformation<>(session,
this.sessionRepository));
}
}
return infos;
}
public SessionInformation getSessionInformation(String sessionId) {
S session = this.sessionRepository.getSession(sessionId);
if (session != null) {
return new SpringSessionBackedSessionInformation<>(session,
this.sessionRepository);
}
return null;
}
/*
* This is a no-op, as we don't administer sessions ourselves.
*/
public void refreshLastRequest(String sessionId) {
}
/*
* This is a no-op, as we don't administer sessions ourselves.
*/
public void registerNewSession(String sessionId, Object principal) {
}
/*
* This is a no-op, as we don't administer sessions ourselves.
*/
public void removeSessionInformation(String sessionId) {
}
/**
* Derives a String name for the given principal.
*
* @param principal as provided by Spring Security
* @return name of the principal, or its {@code toString()} representation if no name
* could be derived
*/
protected String name(Object principal) {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return principal.toString();
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.security.web.authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.util.Assert;
/**
* A {@link RememberMeServices} implementation that uses Spring Session backed
* {@link HttpSession} to provide remember-me service capabilities.
*
* @author Vedran Pavic
* @since 1.3.0
*/
public class SpringSessionRememberMeServices
implements RememberMeServices, LogoutHandler {
/**
* Remember-me login request attribute name.
*/
public static final String REMEMBER_ME_LOGIN_ATTR = SpringSessionRememberMeServices.class
.getName() + "REMEMBER_ME_LOGIN_ATTR";
private static final String DEFAULT_REMEMBERME_PARAMETER = "remember-me";
private static final int THIRTY_DAYS_SECONDS = 2592000;
private static final Log logger = LogFactory
.getLog(SpringSessionRememberMeServices.class);
private String rememberMeParameterName = DEFAULT_REMEMBERME_PARAMETER;
private boolean alwaysRemember;
private int validitySeconds = THIRTY_DAYS_SECONDS;
private String sessionAttrToDeleteOnLoginFail = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
return null;
}
public final void loginFail(HttpServletRequest request,
HttpServletResponse response) {
logout(request);
}
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.alwaysRemember
&& !rememberMeRequested(request, this.rememberMeParameterName)) {
logger.debug("Remember-me login not requested.");
return;
}
request.setAttribute(REMEMBER_ME_LOGIN_ATTR, true);
request.getSession().setMaxInactiveInterval(this.validitySeconds);
}
/**
* Allows customization of whether a remember-me login has been requested. The default
* is to return {@code true} if the configured parameter name has been included in the
* request and is set to the value {@code true}.
* @param request the request submitted from an interactive login, which may include
* additional information indicating that a persistent login is desired.
* @param parameter the configured remember-me parameter name.
* @return true if the request includes information indicating that a persistent login
* has been requested.
*/
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
String rememberMe = request.getParameter(parameter);
if (rememberMe != null) {
if (rememberMe.equalsIgnoreCase("true") || rememberMe.equalsIgnoreCase("on")
|| rememberMe.equalsIgnoreCase("yes") || rememberMe.equals("1")) {
return true;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set "
+ "parameter '" + parameter + "')");
}
return false;
}
/**
* Set the name of the parameter which should be checked for to see if a remember-me
* has been requested during a login request. This should be the same name you assign
* to the checkbox in your login form.
* @param rememberMeParameterName the request parameter
*/
public void setRememberMeParameterName(String rememberMeParameterName) {
Assert.hasText(rememberMeParameterName,
"rememberMeParameterName cannot be empty or null");
this.rememberMeParameterName = rememberMeParameterName;
}
public void setAlwaysRemember(boolean alwaysRemember) {
this.alwaysRemember = alwaysRemember;
}
public void setValiditySeconds(int validitySeconds) {
this.validitySeconds = validitySeconds;
}
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
logout(request);
}
private void logout(HttpServletRequest request) {
logger.debug("Interactive login attempt was unsuccessful.");
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttrToDeleteOnLoginFail);
}
}
}

View File

@@ -0,0 +1,286 @@
/*
* 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.web.context;
import java.util.Arrays;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Conventions;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.util.Assert;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.AbstractContextLoaderInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;
/**
* Registers the {@link DelegatingFilterProxy} to use the springSessionRepositoryFilter
* before any other registered {@link Filter}. When used with
* {@link #AbstractHttpSessionApplicationInitializer(Class...)}, it will also register a
* {@link ContextLoaderListener}. When used with
* {@link #AbstractHttpSessionApplicationInitializer()}, this class is typically used in
* addition to a subclass of {@link AbstractContextLoaderInitializer}.
*
* <p>
* By default the {@link DelegatingFilterProxy} is registered with support for
* asynchronous requests, but can be enabled by overriding
* {@link #isAsyncSessionSupported()} and {@link #getSessionDispatcherTypes()}.
* </p>
*
* <p>
* Additional configuration before and after the springSecurityFilterChain can be added by
* overriding {@link #afterSessionRepositoryFilter(ServletContext)}.
* </p>
*
*
* <h2>Caveats</h2>
* <p>
* Subclasses of {@code AbstractDispatcherServletInitializer} will register their filters
* before any other {@link Filter}. This means that you will typically want to ensure
* subclasses of {@code AbstractDispatcherServletInitializer} are invoked first. This can
* be done by ensuring the {@link Order} or {@link Ordered} of
* {@code AbstractDispatcherServletInitializer} are sooner than subclasses of
* {@code AbstractSecurityWebApplicationInitializer}.
* </p>
*
* @author Rob Winch
*
*/
@Order(100)
public abstract class AbstractHttpSessionApplicationInitializer
implements WebApplicationInitializer {
private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";
/**
* The default name for Spring Session's repository filter.
*/
public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";
private final Class<?>[] configurationClasses;
/**
* Creates a new instance that assumes the Spring Session configuration is loaded by
* some other means than this class. For example, a user might create a
* {@link ContextLoaderListener} using a subclass of
* {@link AbstractContextLoaderInitializer}.
*
* @see ContextLoaderListener
*/
protected AbstractHttpSessionApplicationInitializer() {
this.configurationClasses = null;
}
/**
* Creates a new instance that will instantiate the {@link ContextLoaderListener} with
* the specified classes.
*
* @param configurationClasses {@code @Configuration} classes that will be used to
* configure the context
*/
protected AbstractHttpSessionApplicationInitializer(
Class<?>... configurationClasses) {
this.configurationClasses = configurationClasses;
}
public void onStartup(ServletContext servletContext) throws ServletException {
beforeSessionRepositoryFilter(servletContext);
if (this.configurationClasses != null) {
AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
rootAppContext.register(this.configurationClasses);
servletContext.addListener(new ContextLoaderListener(rootAppContext));
}
insertSessionRepositoryFilter(servletContext);
afterSessionRepositoryFilter(servletContext);
}
/**
* Registers the springSessionRepositoryFilter.
* @param servletContext the {@link ServletContext}
*/
private void insertSessionRepositoryFilter(ServletContext servletContext) {
String filterName = DEFAULT_FILTER_NAME;
DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
filterName);
String contextAttribute = getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSessionRepositoryFilter.setContextAttribute(contextAttribute);
}
registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
}
/**
* Inserts the provided {@link Filter}s before existing {@link Filter}s using default
* generated names, {@link #getSessionDispatcherTypes()}, and
* {@link #isAsyncSessionSupported()}.
*
* @param servletContext the {@link ServletContext} to use
* @param filters the {@link Filter}s to register
*/
protected final void insertFilters(ServletContext servletContext, Filter... filters) {
registerFilters(servletContext, true, filters);
}
/**
* Inserts the provided {@link Filter}s after existing {@link Filter}s using default
* generated names, {@link #getSessionDispatcherTypes()}, and
* {@link #isAsyncSessionSupported()}.
*
* @param servletContext the {@link ServletContext} to use
* @param filters the {@link Filter}s to register
*/
protected final void appendFilters(ServletContext servletContext, Filter... filters) {
registerFilters(servletContext, false, filters);
}
/**
* Registers the provided {@link Filter}s using default generated names,
* {@link #getSessionDispatcherTypes()}, and {@link #isAsyncSessionSupported()}.
*
* @param servletContext the {@link ServletContext} to use
* @param insertBeforeOtherFilters if true, will insert the provided {@link Filter}s
* before other {@link Filter}s. Otherwise, will insert the {@link Filter}s after
* other {@link Filter}s.
* @param filters the {@link Filter}s to register
*/
private void registerFilters(ServletContext servletContext,
boolean insertBeforeOtherFilters, Filter... filters) {
Assert.notEmpty(filters, "filters cannot be null or empty");
for (Filter filter : filters) {
if (filter == null) {
throw new IllegalArgumentException(
"filters cannot contain null values. Got "
+ Arrays.asList(filters));
}
String filterName = Conventions.getVariableName(filter);
registerFilter(servletContext, insertBeforeOtherFilters, filterName, filter);
}
}
/**
* Registers the provided filter using the {@link #isAsyncSessionSupported()} and
* {@link #getSessionDispatcherTypes()}.
*
* @param servletContext the servlet context
* @param insertBeforeOtherFilters should this Filter be inserted before or after
* other {@link Filter}
* @param filterName the filter name
* @param filter the filter
*/
private void registerFilter(ServletContext servletContext,
boolean insertBeforeOtherFilters, String filterName, Filter filter) {
Dynamic registration = servletContext.addFilter(filterName, filter);
if (registration == null) {
throw new IllegalStateException(
"Duplicate Filter registration for '" + filterName
+ "'. Check to ensure the Filter is only configured once.");
}
registration.setAsyncSupported(isAsyncSessionSupported());
EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
"/*");
}
/**
* Returns the {@link DelegatingFilterProxy#getContextAttribute()} or null if the
* parent {@link ApplicationContext} should be used. The default behavior is to use
* the parent {@link ApplicationContext}.
*
* <p>
* If {@link #getDispatcherWebApplicationContextSuffix()} is non-null the
* {@link WebApplicationContext} for the Dispatcher will be used. This means the child
* {@link ApplicationContext} is used to look up the springSessionRepositoryFilter
* bean.
* </p>
*
* @return the {@link DelegatingFilterProxy#getContextAttribute()} or null if the
* parent {@link ApplicationContext} should be used
*/
private String getWebApplicationContextAttribute() {
String dispatcherServletName = getDispatcherWebApplicationContextSuffix();
if (dispatcherServletName == null) {
return null;
}
return SERVLET_CONTEXT_PREFIX + dispatcherServletName;
}
/**
* Return the {@code <servlet-name>} to use the DispatcherServlet's
* {@link WebApplicationContext} to find the {@link DelegatingFilterProxy} or null to
* use the parent {@link ApplicationContext}.
*
* <p>
* For example, if you are using AbstractDispatcherServletInitializer or
* AbstractAnnotationConfigDispatcherServletInitializer and using the provided Servlet
* name, you can return "dispatcher" from this method to use the DispatcherServlet's
* {@link WebApplicationContext}.
* </p>
*
* @return the {@code <servlet-name>} of the DispatcherServlet to use its
* {@link WebApplicationContext} or null (default) to use the parent
* {@link ApplicationContext}.
*/
protected String getDispatcherWebApplicationContextSuffix() {
return null;
}
/**
* Invoked before the springSessionRepositoryFilter is added.
* @param servletContext the {@link ServletContext}
*/
protected void beforeSessionRepositoryFilter(ServletContext servletContext) {
}
/**
* Invoked after the springSessionRepositoryFilter is added.
* @param servletContext the {@link ServletContext}
*/
protected void afterSessionRepositoryFilter(ServletContext servletContext) {
}
/**
* Get the {@link DispatcherType} for the springSessionRepositoryFilter.
* @return the {@link DispatcherType} for the filter
*/
protected EnumSet<DispatcherType> getSessionDispatcherTypes() {
return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR,
DispatcherType.ASYNC);
}
/**
* Determine if the springSessionRepositoryFilter should be marked as supporting
* asynch. Default is true.
*
* @return true if springSessionRepositoryFilter should be marked as supporting asynch
*/
protected boolean isAsyncSessionSupported() {
return true;
}
}

View File

@@ -0,0 +1,447 @@
/*
* Copyright 2014-2017 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.web.http;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.session.Session;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.util.Assert;
/**
* A {@link HttpSessionStrategy} that uses a cookie to obtain the session from.
* Specifically, this implementation will allow specifying a cookie serialization strategy
* using {@link CookieHttpSessionStrategy#setCookieSerializer(CookieSerializer)}. The
* default is cookie name is "SESSION".
*
* When a session is created, the HTTP response will have a cookie with the specified
* cookie name and the value of the session id. The cookie will be marked as a session
* cookie, use the context path for the path of the cookie, marked as HTTPOnly, and if
* {@link javax.servlet.http.HttpServletRequest#isSecure()} returns true, the cookie will
* be marked as secure. For example:
*
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
* </pre>
*
* The client should now include the session in each request by specifying the same cookie
* in their request. For example:
*
* <pre>
* GET /messages/ HTTP/1.1
* Host: example.com
* Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* When the session is invalidated, the server will send an HTTP response that expires the
* cookie. For example:
*
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
* </pre>
*
* <h2>Supporting Multiple Simultaneous Sessions</h2>
*
* <p>
* By default multiple sessions are also supported. Once a session is established with the
* browser, another session can be initiated by specifying a unique value for the
* {@link #setSessionAliasParamName(String)}. For example, a request to:
* </p>
*
* <pre>
* GET /messages/?_s=1416195761178 HTTP/1.1
* Host: example.com
* Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* Will result in the following response:
*
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
* </pre>
*
* <p>
* To use the original session a request without the HTTP parameter u can be made. To use
* the new session, a request with the HTTP parameter _s=1416195761178 can be used. By
* default URLs will be rewritten to include the currently selected session.
* </p>
*
* <h2>Selecting Sessions</h2>
*
* <p>
* Sessions can be managed by using the HttpSessionManager and SessionRepository. If you
* are not using Spring in the rest of your application you can obtain a reference from
* the HttpServletRequest attributes. An example is provided below:
* </p>
*
* <code>
* HttpSessionManager sessionManager =
* (HttpSessionManager) req.getAttribute(HttpSessionManager.class.getName());
* SessionRepository&lt;Session&gt; repo =
* (SessionRepository&lt;Session&gt;) req.getAttribute(SessionRepository.class.getName());
*
* String currentSessionAlias = sessionManager.getCurrentSessionAlias(req);
* Map&lt;String, String&gt; sessionIds = sessionManager.getSessionIds(req);
* String newSessionAlias = String.valueOf(System.currentTimeMillis());
*
* String contextPath = req.getContextPath();
* List&lt;Account&gt; accounts = new ArrayList&lt;&gt;();
* Account currentAccount = null; for(Map.Entry&lt;String, String&gt; entry :
* sessionIds.entrySet()) { String alias = entry.getKey(); String sessionId =
* entry.getValue();
* </code>
*
* Session session = repo.getSession(sessionId); if(session == null) { continue; }
*
* String username = session.getAttribute("username"); if(username == null) {
* newSessionAlias = alias; continue; }
*
* String logoutUrl = sessionManager.encodeURL("./logout", alias); String switchAccountUrl
* = sessionManager.encodeURL("./", alias); Account account = new Account(username,
* logoutUrl, switchAccountUrl); if(currentSessionAlias.equals(alias)) { currentAccount =
* account; } else { accounts.add(account); } }
*
* req.setAttribute("currentAccount", currentAccount); req.setAttribute("addAccountUrl",
* sessionManager.encodeURL(contextPath, newSessionAlias)); req.setAttribute("accounts",
* accounts); }
*
*
* @author Rob Winch
* @since 1.0
*/
public final class CookieHttpSessionStrategy
implements MultiHttpSessionStrategy, HttpSessionManager {
/**
* The default delimiter for both serialization and deserialization.
*/
private static final String DEFAULT_DELIMITER = " ";
private static final String SESSION_IDS_WRITTEN_ATTR = CookieHttpSessionStrategy.class
.getName().concat(".SESSIONS_WRITTEN_ATTR");
static final String DEFAULT_ALIAS = "0";
static final String DEFAULT_SESSION_ALIAS_PARAM_NAME = "_s";
private static final Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
/**
* The delimiter between a session alias and a session id when reading a cookie value.
* The default value is " ".
*/
private String deserializationDelimiter = DEFAULT_DELIMITER;
/**
* The delimiter between a session alias and a session id when writing a cookie value.
* The default is " ".
*/
private String serializationDelimiter = DEFAULT_DELIMITER;
public String getRequestedSessionId(HttpServletRequest request) {
Map<String, String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
return sessionIds.get(sessionAlias);
}
public String getCurrentSessionAlias(HttpServletRequest request) {
if (this.sessionParam == null) {
return DEFAULT_ALIAS;
}
String u = request.getParameter(this.sessionParam);
if (u == null) {
return DEFAULT_ALIAS;
}
if (!ALIAS_PATTERN.matcher(u).matches()) {
return DEFAULT_ALIAS;
}
return u;
}
public String getNewSessionAlias(HttpServletRequest request) {
Set<String> sessionAliases = getSessionIds(request).keySet();
if (sessionAliases.isEmpty()) {
return DEFAULT_ALIAS;
}
long lastAlias = Long.decode(DEFAULT_ALIAS);
for (String alias : sessionAliases) {
long selectedAlias = safeParse(alias);
if (selectedAlias > lastAlias) {
lastAlias = selectedAlias;
}
}
return Long.toHexString(lastAlias + 1);
}
private long safeParse(String hex) {
try {
return Long.decode("0x" + hex);
}
catch (NumberFormatException notNumber) {
return 0;
}
}
public void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response) {
Set<String> sessionIdsWritten = getSessionIdsWritten(request);
if (sessionIdsWritten.contains(session.getId())) {
return;
}
sessionIdsWritten.add(session.getId());
Map<String, String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
sessionIds.put(sessionAlias, session.getId());
String cookieValue = createSessionCookieValue(sessionIds);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
@SuppressWarnings("unchecked")
private Set<String> getSessionIdsWritten(HttpServletRequest request) {
Set<String> sessionsWritten = (Set<String>) request
.getAttribute(SESSION_IDS_WRITTEN_ATTR);
if (sessionsWritten == null) {
sessionsWritten = new HashSet<>();
request.setAttribute(SESSION_IDS_WRITTEN_ATTR, sessionsWritten);
}
return sessionsWritten;
}
private String createSessionCookieValue(Map<String, String> sessionIds) {
if (sessionIds.isEmpty()) {
return "";
}
if (sessionIds.size() == 1 && sessionIds.keySet().contains(DEFAULT_ALIAS)) {
return sessionIds.values().iterator().next();
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sessionIds.entrySet()) {
String alias = entry.getKey();
String id = entry.getValue();
sb.append(alias);
sb.append(this.serializationDelimiter);
sb.append(id);
sb.append(this.serializationDelimiter);
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
Map<String, String> sessionIds = getSessionIds(request);
String requestedAlias = getCurrentSessionAlias(request);
sessionIds.remove(requestedAlias);
String cookieValue = createSessionCookieValue(sessionIds);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
/**
* Sets the name of the HTTP parameter that is used to specify the session alias. If
* the value is null, then only a single session is supported per browser.
*
* @param sessionAliasParamName the name of the HTTP parameter used to specify the
* session alias. If null, then ony a single session is supported per browser.
*/
public void setSessionAliasParamName(String sessionAliasParamName) {
this.sessionParam = sessionAliasParamName;
}
/**
* Sets the {@link CookieSerializer} to be used.
*
* @param cookieSerializer the cookieSerializer to set. Cannot be null.
*/
public void setCookieSerializer(CookieSerializer cookieSerializer) {
Assert.notNull(cookieSerializer, "cookieSerializer cannot be null");
this.cookieSerializer = cookieSerializer;
}
/**
* Sets the delimiter between a session alias and a session id when deserializing a
* cookie. The default is " " This is useful when using
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> for writing the cookies
* which doesn't allow for spaces in the cookie values.
*
* @param delimiter the delimiter to set (i.e. "_ " will try a delimeter of either "_"
* or " ")
*/
public void setDeserializationDelimiter(String delimiter) {
this.deserializationDelimiter = delimiter;
}
/**
* Sets the delimiter between a session alias and a session id when deserializing a
* cookie. The default is " ". This is useful when using
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> for writing the cookies
* which doesn't allow for spaces in the cookie values.
*
* @param delimiter the delimiter to set (i.e. "_")
*/
public void setSerializationDelimiter(String delimiter) {
this.serializationDelimiter = delimiter;
}
public Map<String, String> getSessionIds(HttpServletRequest request) {
List<String> cookieValues = this.cookieSerializer.readCookieValues(request);
String sessionCookieValue = cookieValues.isEmpty() ? ""
: cookieValues.iterator().next();
Map<String, String> result = new LinkedHashMap<>();
StringTokenizer tokens = new StringTokenizer(sessionCookieValue,
this.deserializationDelimiter);
if (tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
while (tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if (!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
public HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response) {
request.setAttribute(HttpSessionManager.class.getName(), this);
return request;
}
public HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response) {
return new MultiSessionHttpServletResponse(response, request);
}
public String encodeURL(String url, String sessionAlias) {
String encodedSessionAlias = urlEncode(sessionAlias);
int queryStart = url.indexOf("?");
boolean isDefaultAlias = DEFAULT_ALIAS.equals(encodedSessionAlias);
if (queryStart < 0) {
return isDefaultAlias ? url
: url + "?" + this.sessionParam + "=" + encodedSessionAlias;
}
String path = url.substring(0, queryStart);
String query = url.substring(queryStart + 1, url.length());
String replacement = isDefaultAlias ? "" : "$1" + encodedSessionAlias;
query = query.replaceFirst("((^|&)" + this.sessionParam + "=)([^&]+)?",
replacement);
String sessionParamReplacement = String.format("%s=%s", this.sessionParam,
encodedSessionAlias);
if (!isDefaultAlias && !query.contains(sessionParamReplacement)
&& url.endsWith(query)) {
// no existing alias
if (!(query.endsWith("&") || query.length() == 0)) {
query += "&";
}
query += sessionParamReplacement;
}
return path + "?" + query;
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* A {@link CookieHttpSessionStrategy} aware {@link HttpServletResponseWrapper}.
*/
class MultiSessionHttpServletResponse extends HttpServletResponseWrapper {
private final HttpServletRequest request;
MultiSessionHttpServletResponse(HttpServletResponse response,
HttpServletRequest request) {
super(response);
this.request = request;
}
private String getCurrentSessionAliasFromUrl(String url) {
String currentSessionAliasFromUrl = null;
int queryStart = url.indexOf("?");
if (queryStart >= 0) {
String query = url.substring(queryStart + 1);
Matcher matcher = Pattern
.compile(String.format("%s=([^&]+)",
CookieHttpSessionStrategy.this.sessionParam))
.matcher(query);
if (matcher.find()) {
currentSessionAliasFromUrl = matcher.group(1);
}
}
return currentSessionAliasFromUrl;
}
@Override
public String encodeRedirectURL(String url) {
String encodedUrl = super.encodeRedirectURL(url);
String currentSessionAliasFromUrl = getCurrentSessionAliasFromUrl(encodedUrl);
String alias = (currentSessionAliasFromUrl != null)
? currentSessionAliasFromUrl : getCurrentSessionAlias(this.request);
return CookieHttpSessionStrategy.this.encodeURL(encodedUrl, alias);
}
@Override
public String encodeURL(String url) {
String encodedUrl = super.encodeURL(url);
String currentSessionAliasFromUrl = getCurrentSessionAliasFromUrl(encodedUrl);
String alias = (currentSessionAliasFromUrl != null)
? currentSessionAliasFromUrl : getCurrentSessionAlias(this.request);
return CookieHttpSessionStrategy.this.encodeURL(encodedUrl, alias);
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.web.http;
import java.util.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Strategy for reading and writing a cookie value to the {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.1
*/
public interface CookieSerializer {
/**
* Writes a given {@link CookieValue} to the provided {@link HttpServletResponse}.
*
* @param cookieValue the {@link CookieValue} to write to
* {@link CookieValue#getResponse()}. Cannot be null.
*/
void writeCookieValue(CookieValue cookieValue);
/**
* Reads all the matching cookies from the {@link HttpServletRequest}. The result is a
* List since there can be multiple {@link Cookie} in a single request with a matching
* name. For example, one Cookie may have a path of / and another of /context, but the
* path is not transmitted in the request.
*
* @param request the {@link HttpServletRequest} to read the cookie from. Cannot be
* null.
* @return the values of all the matching cookies
*/
List<String> readCookieValues(HttpServletRequest request);
/**
* Contains the information necessary to write a value to the
* {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.1
*/
class CookieValue {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final String cookieValue;
/**
* Creates a new instance.
*
* @param request the {@link HttpServletRequest} to use. Useful for determining
* the context in which the cookie is set. Cannot be null.
* @param response the {@link HttpServletResponse} to use.
* @param cookieValue the value of the cookie to be written. This value may be
* modified by the {@link CookieSerializer} when writing to the actual cookie so
* long as the original value is returned when the cookie is read.
*/
public CookieValue(HttpServletRequest request, HttpServletResponse response,
String cookieValue) {
this.request = request;
this.response = response;
this.cookieValue = cookieValue;
}
/**
* Gets the request to use.
* @return the request to use. Cannot be null.
*/
public HttpServletRequest getRequest() {
return this.request;
}
/**
* Gets the response to write to.
* @return the response to write to. Cannot be null.
*/
public HttpServletResponse getResponse() {
return this.response;
}
/**
* The value to be written. This value may be modified by the
* {@link CookieSerializer} before written to the cookie. However, the value must
* be the same as the original when it is read back in.
*
* @return the value to be written
*/
public String getCookieValue() {
return this.cookieValue;
}
}
}

View File

@@ -0,0 +1,353 @@
/*
* Copyright 2014-2017 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.web.http;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* The default implementation of {@link CookieSerializer}.
*
* @author Rob Winch
* @author Vedran Pavic
* @author Eddú Meléndez
* @since 1.1
*/
public class DefaultCookieSerializer implements CookieSerializer {
private String cookieName = "SESSION";
private Boolean useSecureCookie;
private boolean useHttpOnlyCookie = isServlet3();
private String cookiePath;
private int cookieMaxAge = -1;
private String domainName;
private Pattern domainNamePattern;
private String jvmRoute;
private boolean useBase64Encoding = true;
private String rememberMeRequestAttribute;
/*
* (non-Javadoc)
*
* @see org.springframework.session.web.http.CookieSerializer#readCookieValues(javax.
* servlet.http.HttpServletRequest)
*/
public List<String> readCookieValues(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
List<String> matchingCookieValues = new ArrayList<>();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (this.cookieName.equals(cookie.getName())) {
String sessionId = this.useBase64Encoding
? base64Decode(cookie.getValue()) : cookie.getValue();
if (sessionId == null) {
continue;
}
if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0,
sessionId.length() - this.jvmRoute.length());
}
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
/*
* (non-Javadoc)
*
* @see org.springframework.session.web.http.CookieWriter#writeCookieValue(org.
* springframework.session.web.http.CookieWriter.CookieValue)
*/
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = this.jvmRoute == null ? requestedCookieValue
: requestedCookieValue + this.jvmRoute;
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding
? base64Encode(actualCookieValue) : actualCookieValue);
sessionCookie.setSecure(isSecureCookie(request));
sessionCookie.setPath(getCookiePath(request));
String domainName = getDomainName(request);
if (domainName != null) {
sessionCookie.setDomain(domainName);
}
if (this.useHttpOnlyCookie) {
sessionCookie.setHttpOnly(true);
}
if ("".equals(requestedCookieValue)) {
sessionCookie.setMaxAge(0);
}
else if (this.rememberMeRequestAttribute != null
&& request.getAttribute(this.rememberMeRequestAttribute) != null) {
// the cookie is only written at time of session creation, so we rely on
// session expiration rather than cookie expiration if remember me is enabled
sessionCookie.setMaxAge(Integer.MAX_VALUE);
}
else {
sessionCookie.setMaxAge(this.cookieMaxAge);
}
response.addCookie(sessionCookie);
}
/**
* Decode the value using Base64.
* @param base64Value the Base64 String to decode
* @return the Base64 decoded value
* @since 1.2.2
*/
private String base64Decode(String base64Value) {
try {
byte[] decodedCookieBytes = Base64.getDecoder().decode(base64Value);
return new String(decodedCookieBytes);
}
catch (Exception e) {
return null;
}
}
/**
* Encode the value using Base64.
* @param value the String to Base64 encode
* @return the Base64 encoded value
* @since 1.2.2
*/
private String base64Encode(String value) {
byte[] encodedCookieBytes = Base64.getEncoder().encode(value.getBytes());
return new String(encodedCookieBytes);
}
/**
* Sets if a Cookie marked as secure should be used. The default is to use the value
* of {@link HttpServletRequest#isSecure()}.
*
* @param useSecureCookie determines if the cookie should be marked as secure.
*/
public void setUseSecureCookie(boolean useSecureCookie) {
this.useSecureCookie = useSecureCookie;
}
/**
* Sets if a Cookie marked as HTTP Only should be used. The default is true in Servlet
* 3+ environments, else false.
*
* @param useHttpOnlyCookie determines if the cookie should be marked as HTTP Only.
*/
public void setUseHttpOnlyCookie(boolean useHttpOnlyCookie) {
if (useHttpOnlyCookie && !isServlet3()) {
throw new IllegalArgumentException(
"You cannot set useHttpOnlyCookie to true in pre Servlet 3 environment");
}
this.useHttpOnlyCookie = useHttpOnlyCookie;
}
private boolean isSecureCookie(HttpServletRequest request) {
if (this.useSecureCookie == null) {
return request.isSecure();
}
return this.useSecureCookie;
}
/**
* Sets the path of the Cookie. The default is to use the context path from the
* {@link HttpServletRequest}.
*
* @param cookiePath the path of the Cookie. If null, the default of the context path
* will be used.
*/
public void setCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
}
public void setCookieName(String cookieName) {
if (cookieName == null) {
throw new IllegalArgumentException("cookieName cannot be null");
}
this.cookieName = cookieName;
}
/**
* Sets the maxAge property of the Cookie. The default is -1 which signals to delete
* the cookie when the browser is closed.
*
* @param cookieMaxAge the maxAge property of the Cookie
*/
public void setCookieMaxAge(int cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
}
/**
* Sets an explicit Domain Name. This allow the domain of "example.com" to be used
* when the request comes from www.example.com. This allows for sharing the cookie
* across subdomains. The default is to use the current domain.
*
* @param domainName the name of the domain to use. (i.e. "example.com")
* @throws IllegalStateException if the domainNamePattern is also set
*/
public void setDomainName(String domainName) {
if (this.domainNamePattern != null) {
throw new IllegalStateException(
"Cannot set both domainName and domainNamePattern");
}
this.domainName = domainName;
}
/**
* <p>
* Sets a case insensitive pattern used to extract the domain name from the
* {@link HttpServletRequest#getServerName()}. The pattern should provide a single
* grouping that defines what the value is that should be matched. User's should be
* careful not to output malicious characters like new lines to prevent from things
* like <a href= "https://www.owasp.org/index.php/HTTP_Response_Splitting">HTTP
* Response Splitting</a>.
* </p>
*
* <p>
* If the pattern does not match, then no domain will be set. This is useful to ensure
* the domain is not set during development when localhost might be used.
* </p>
* <p>
* An example value might be "^.+?\\.(\\w+\\.[a-z]+)$". For the given input, it would
* provide the following explicit domain (null means no domain name is set):
* </p>
*
* <ul>
* <li>example.com - null</li>
* <li>child.sub.example.com - example.com</li>
* <li>localhost - null</li>
* <li>127.0.1.1 - null</li>
* </ul>
*
* @param domainNamePattern the case insensitive pattern to extract the domain name
* with
* @throws IllegalStateException if the domainName is also set
*/
public void setDomainNamePattern(String domainNamePattern) {
if (this.domainName != null) {
throw new IllegalStateException(
"Cannot set both domainName and domainNamePattern");
}
this.domainNamePattern = Pattern.compile(domainNamePattern,
Pattern.CASE_INSENSITIVE);
}
/**
* <p>
* Used to identify which JVM to route to for session affinity. With some
* implementations (i.e. Redis) this provides no performance benefit. However, this
* can help with tracing logs of a particular user. This will ensure that the value of
* the cookie is formatted as
* </p>
* <code>
* sessionId + "." jvmRoute
* </code>
* <p>
* To use set a custom route on each JVM instance and setup a frontend proxy to
* forward all requests to the JVM based on the route.
* </p>
*
* @param jvmRoute the JVM Route to use (i.e. "node01jvmA", "n01ja", etc)
*/
public void setJvmRoute(String jvmRoute) {
this.jvmRoute = "." + jvmRoute;
}
/**
* Set if the Base64 encoding of cookie value should be used. This is valuable in
* order to support <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> which
* recommends using Base 64 encoding to the cookie value.
*
* @param useBase64Encoding the flag to indicate whether to use Base64 encoding
*/
public void setUseBase64Encoding(boolean useBase64Encoding) {
this.useBase64Encoding = useBase64Encoding;
}
/**
* Set the request attribute name that indicates remember-me login. If specified, the
* cookie will be written as Integer.MAX_VALUE.
* @param rememberMeRequestAttribute the remember-me request attribute name
* @since 1.3.0
*/
public void setRememberMeRequestAttribute(String rememberMeRequestAttribute) {
if (rememberMeRequestAttribute == null) {
throw new IllegalArgumentException(
"rememberMeRequestAttribute cannot be null");
}
this.rememberMeRequestAttribute = rememberMeRequestAttribute;
}
private String getDomainName(HttpServletRequest request) {
if (this.domainName != null) {
return this.domainName;
}
if (this.domainNamePattern != null) {
Matcher matcher = this.domainNamePattern.matcher(request.getServerName());
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
private String getCookiePath(HttpServletRequest request) {
if (this.cookiePath == null) {
return request.getContextPath() + "/";
}
return this.cookiePath;
}
/**
* Returns true if the Servlet 3 APIs are detected.
*
* @return whether the Servlet 3 APIs are detected
*/
private boolean isServlet3() {
try {
ServletRequest.class.getMethod("startAsync");
return true;
}
catch (NoSuchMethodException e) {
}
return false;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2014-2017 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.web.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link HttpSessionStrategy} that uses a header to obtain the session from.
* Specifically, this implementation will allow specifying a header name using
* {@link HeaderHttpSessionStrategy#setHeaderName(String)}. The default is "X-Auth-Token".
*
* When a session is created, the HTTP response will have a response header of the
* specified name and the value of the session id. For example:
*
* <pre>
* HTTP/1.1 200 OK
* X-Auth-Token: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* The client should now include the session in each request by specifying the same header
* in their request. For example:
*
* <pre>
* GET /messages/ HTTP/1.1
* Host: example.com
* X-Auth-Token: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* When the session is invalidated, the server will send an HTTP response that has the
* header name and a blank value. For example:
*
* <pre>
* HTTP/1.1 200 OK
* X-Auth-Token:
* </pre>
*
* @author Rob Winch
* @since 1.0
*/
public class HeaderHttpSessionStrategy implements HttpSessionStrategy {
private String headerName = "X-Auth-Token";
public String getRequestedSessionId(HttpServletRequest request) {
return request.getHeader(this.headerName);
}
public void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response) {
response.setHeader(this.headerName, session.getId());
}
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
response.setHeader(this.headerName, "");
}
/**
* The name of the header to obtain the session id from. Default is "X-Auth-Token".
*
* @param headerName the name of the header to obtain the session id from.
*/
public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName cannot be null");
this.headerName = headerName;
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2014-2017 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.web.http;
import java.time.Duration;
import java.util.Collections;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.springframework.session.Session;
/**
* Adapts Spring Session's {@link Session} to an {@link HttpSession}.
*
* @param <S> the {@link Session} type
* @author Rob Winch
* @since 1.1
*/
@SuppressWarnings("deprecation")
class HttpSessionAdapter<S extends Session> implements HttpSession {
private S session;
private final ServletContext servletContext;
private boolean invalidated;
private boolean old;
HttpSessionAdapter(S session, ServletContext servletContext) {
this.session = session;
this.servletContext = servletContext;
}
public void setSession(S session) {
this.session = session;
}
public S getSession() {
return this.session;
}
public long getCreationTime() {
checkState();
return this.session.getCreationTime().toEpochMilli();
}
public String getId() {
return this.session.getId();
}
public long getLastAccessedTime() {
checkState();
return this.session.getLastAccessedTime().toEpochMilli();
}
public ServletContext getServletContext() {
return this.servletContext;
}
public void setMaxInactiveInterval(int interval) {
this.session.setMaxInactiveInterval(Duration.ofSeconds(interval));
}
public int getMaxInactiveInterval() {
return (int) this.session.getMaxInactiveInterval().getSeconds();
}
public HttpSessionContext getSessionContext() {
return NOOP_SESSION_CONTEXT;
}
public Object getAttribute(String name) {
checkState();
return this.session.getAttribute(name).orElse(null);
}
public Object getValue(String name) {
return getAttribute(name);
}
public Enumeration<String> getAttributeNames() {
checkState();
return Collections.enumeration(this.session.getAttributeNames());
}
public String[] getValueNames() {
checkState();
Set<String> attrs = this.session.getAttributeNames();
return attrs.toArray(new String[0]);
}
public void setAttribute(String name, Object value) {
checkState();
this.session.setAttribute(name, value);
}
public void putValue(String name, Object value) {
setAttribute(name, value);
}
public void removeAttribute(String name) {
checkState();
this.session.removeAttribute(name);
}
public void removeValue(String name) {
removeAttribute(name);
}
public void invalidate() {
checkState();
this.invalidated = true;
}
public void setNew(boolean isNew) {
this.old = !isNew;
}
public boolean isNew() {
checkState();
return !this.old;
}
private void checkState() {
if (this.invalidated) {
throw new IllegalStateException(
"The HttpSession has already be invalidated.");
}
}
private static final HttpSessionContext NOOP_SESSION_CONTEXT = new HttpSessionContext() {
public HttpSession getSession(String sessionId) {
return null;
}
public Enumeration<String> getIds() {
return EMPTY_ENUMERATION;
}
};
private static final Enumeration<String> EMPTY_ENUMERATION = new Enumeration<String>() {
public boolean hasMoreElements() {
return false;
}
public String nextElement() {
throw new NoSuchElementException("a");
}
};
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.web.http;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* Allows managing a mapping of alias to the session id for having multiple active
* sessions at the same time.
*
* @author Rob Winch
* @since 1.0
*
*/
public interface HttpSessionManager {
/**
* Gets the current session's alias from the {@link HttpServletRequest}.
*
* @param request the {@link HttpServletRequest} to obtain the current session's alias
* from.
* @return the current sessions' alias. Cannot be null.
*/
String getCurrentSessionAlias(HttpServletRequest request);
/**
* Gets a mapping of the session alias to the session id from the
* {@link HttpServletRequest}.
*
* @param request the {@link HttpServletRequest} to obtain the mapping from. Cannot be
* null.
* @return a mapping of the session alias to the session id from the
* {@link HttpServletRequest}. Cannot be null.
*/
Map<String, String> getSessionIds(HttpServletRequest request);
/**
* Provides the ability to encode the URL for a given session alias.
*
* @param url the url to encode.
* @param sessionAlias the session alias to encode.
* @return the encoded URL
*/
String encodeURL(String url, String sessionAlias);
/**
* Gets a new and unique Session alias. Typically this will be called to pass into
* {@code HttpSessionManager#encodeURL(java.lang.String)}. For example:
*
* <code>
* String newAlias = httpSessionManager.getNewSessionAlias(request);
* String addAccountUrl = httpSessionManager.encodeURL("./", newAlias);
* </code>
*
* @param request the {@link HttpServletRequest} to get a new alias from
* @return Gets a new and unique Session alias.
*/
String getNewSessionAlias(HttpServletRequest request);
}

View File

@@ -0,0 +1,79 @@
/*
* 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.web.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.session.Session;
/**
* A strategy for mapping HTTP request and responses to a {@link Session}.
*
* @author Rob Winch
* @since 1.0
*/
public interface HttpSessionStrategy {
/**
* Obtains the requested session id from the provided
* {@link javax.servlet.http.HttpServletRequest}. For example, the session id might
* come from a cookie or a request header.
*
* @param request the {@link javax.servlet.http.HttpServletRequest} to obtain the
* session id from. Cannot be null.
* @return the {@link javax.servlet.http.HttpServletRequest} to obtain the session id
* from.
*/
String getRequestedSessionId(HttpServletRequest request);
/**
* This method is invoked when a new session is created and should inform a client
* what the new session id is. For example, it might create a new cookie with the
* session id in it or set an HTTP response header with the value of the new session
* id.
*
* Some implementations may wish to associate additional information to the
* {@link Session} at this time. For example, they may wish to add the IP Address,
* browser headers, the username, etc to the
* {@link org.springframework.session.Session}.
*
* @param session the {@link org.springframework.session.Session} that is being sent
* to the client. Cannot be null.
* @param request the {@link javax.servlet.http.HttpServletRequest} that create the
* new {@link org.springframework.session.Session} Cannot be null.
* @param response the {@link javax.servlet.http.HttpServletResponse} that is
* associated with the {@link javax.servlet.http.HttpServletRequest} that created the
* new {@link org.springframework.session.Session} Cannot be null.
*/
void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response);
/**
* This method is invoked when a session is invalidated and should inform a client
* that the session id is no longer valid. For example, it might remove a cookie with
* the session id in it or set an HTTP response header with an empty value indicating
* to the client to no longer submit that session id.
*
* @param request the {@link javax.servlet.http.HttpServletRequest} that invalidated
* the {@link org.springframework.session.Session} Cannot be null.
* @param response the {@link javax.servlet.http.HttpServletResponse} that is
* associated with the {@link javax.servlet.http.HttpServletRequest} that invalidated
* the {@link org.springframework.session.Session} Cannot be null.
*/
void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);
}

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 org.springframework.session.web.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* Some {@link HttpSessionStrategy} may also want to further customize
* {@link HttpServletRequest} and {@link HttpServletResponse} objects. For example,
* {@link CookieHttpSessionStrategy} customizes how URL rewriting is done to select which
* session should be used in the event multiple sessions are active.
* </p>
*
* @author Rob Winch
* @since 1.0
* @see CookieHttpSessionStrategy
*/
public interface MultiHttpSessionStrategy
extends HttpSessionStrategy, RequestResponsePostProcessor {
}

View File

@@ -0,0 +1,578 @@
/*
* 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.web.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base class for response wrappers which encapsulate the logic for handling an event when
* the {@link javax.servlet.http.HttpServletResponse} is committed.
*
* @author Rob Winch
* @since 1.0
*/
abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
private final Log logger = LogFactory.getLog(getClass());
private boolean disableOnCommitted;
/**
* The Content-Length response header. If this is greater than 0, then once
* {@link #contentWritten} is larger than or equal the response is considered
* committed.
*/
private long contentLength;
/**
* The size of data written to the response body.
*/
private long contentWritten;
/**
* Create a new {@link OnCommittedResponseWrapper}.
* @param response the response to be wrapped
*/
OnCommittedResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void addHeader(String name, String value) {
if ("Content-Length".equalsIgnoreCase(name)) {
setContentLength(Long.parseLong(value));
}
super.addHeader(name, value);
}
@Override
public void setContentLength(int len) {
setContentLength((long) len);
super.setContentLength(len);
}
private void setContentLength(long len) {
this.contentLength = len;
checkContentLength(0);
}
/**
* Invoke this method to disable invoking
* {@link OnCommittedResponseWrapper#onResponseCommitted()} when the
* {@link javax.servlet.http.HttpServletResponse} is committed. This can be useful in
* the event that Async Web Requests are made.
*/
public void disableOnResponseCommitted() {
this.disableOnCommitted = true;
}
/**
* Implement the logic for handling the {@link javax.servlet.http.HttpServletResponse}
* being committed.
*/
protected abstract void onResponseCommitted();
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the superclass <code>sendError()</code>.
* @param sc the error status code
*/
@Override
public final void sendError(int sc) throws IOException {
doOnResponseCommitted();
super.sendError(sc);
}
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the superclass <code>sendError()</code>.
* @param sc the error status code
*/
@Override
public final void sendError(int sc, String msg) throws IOException {
doOnResponseCommitted();
super.sendError(sc, msg);
}
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the superclass <code>sendRedirect()</code>.
* @param location the redirect URL location
*/
@Override
public final void sendRedirect(String location) throws IOException {
doOnResponseCommitted();
super.sendRedirect(location);
}
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the calling <code>getOutputStream().close()</code> or
* <code>getOutputStream().flush()</code>.
* @throws IOException if an input or output exception occurred
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new SaveContextServletOutputStream(super.getOutputStream());
}
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the <code>getWriter().close()</code> or
* <code>getWriter().flush()</code>.
* @throws IOException if an input or output exception occurred
*/
@Override
public PrintWriter getWriter() throws IOException {
return new SaveContextPrintWriter(super.getWriter());
}
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the superclass <code>flushBuffer()</code>.
* @throws IOException if an input or output exception occurred
*/
@Override
public void flushBuffer() throws IOException {
doOnResponseCommitted();
super.flushBuffer();
}
private void trackContentLength(boolean content) {
checkContentLength(content ? 4 : 5); // TODO Localization
}
private void trackContentLength(char content) {
checkContentLength(1);
}
private void trackContentLength(Object content) {
trackContentLength(String.valueOf(content));
}
private void trackContentLength(byte[] content) {
checkContentLength(content == null ? 0 : content.length);
}
private void trackContentLength(char[] content) {
checkContentLength(content == null ? 0 : content.length);
}
private void trackContentLength(int content) {
trackContentLength(String.valueOf(content));
}
private void trackContentLength(float content) {
trackContentLength(String.valueOf(content));
}
private void trackContentLength(double content) {
trackContentLength(String.valueOf(content));
}
private void trackContentLengthLn() {
trackContentLength("\r\n");
}
private void trackContentLength(String content) {
checkContentLength(content.length());
}
/**
* Adds the contentLengthToWrite to the total contentWritten size and checks to see if
* the response should be written.
*
* @param contentLengthToWrite the size of the content that is about to be written.
*/
private void checkContentLength(long contentLengthToWrite) {
this.contentWritten += contentLengthToWrite;
boolean isBodyFullyWritten = this.contentLength > 0
&& this.contentWritten >= this.contentLength;
int bufferSize = getBufferSize();
boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize;
if (isBodyFullyWritten || requiresFlush) {
doOnResponseCommitted();
}
}
/**
* Calls <code>onResponseCommmitted()</code> with the current contents as long as
* {@link #disableOnResponseCommitted()} was not invoked.
*/
private void doOnResponseCommitted() {
if (!this.disableOnCommitted) {
onResponseCommitted();
disableOnResponseCommitted();
}
}
/**
* Ensures {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked before
* calling the prior to methods that commit the response. We delegate all methods to
* the original {@link java.io.PrintWriter} to ensure that the behavior is as close to
* the original {@link java.io.PrintWriter} as possible. See SEC-2039
* @author Rob Winch
*/
private class SaveContextPrintWriter extends PrintWriter {
private final PrintWriter delegate;
SaveContextPrintWriter(PrintWriter delegate) {
super(delegate);
this.delegate = delegate;
}
public void flush() {
doOnResponseCommitted();
this.delegate.flush();
}
public void close() {
doOnResponseCommitted();
this.delegate.close();
}
public int hashCode() {
return this.delegate.hashCode();
}
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
public String toString() {
return getClass().getName() + "[delegate=" + this.delegate.toString() + "]";
}
public boolean checkError() {
return this.delegate.checkError();
}
public void write(int c) {
trackContentLength(c);
this.delegate.write(c);
}
public void write(char[] buf, int off, int len) {
checkContentLength(len);
this.delegate.write(buf, off, len);
}
public void write(char[] buf) {
trackContentLength(buf);
this.delegate.write(buf);
}
public void write(String s, int off, int len) {
checkContentLength(len);
this.delegate.write(s, off, len);
}
public void write(String s) {
trackContentLength(s);
this.delegate.write(s);
}
public void print(boolean b) {
trackContentLength(b);
this.delegate.print(b);
}
public void print(char c) {
trackContentLength(c);
this.delegate.print(c);
}
public void print(int i) {
trackContentLength(i);
this.delegate.print(i);
}
public void print(long l) {
trackContentLength(l);
this.delegate.print(l);
}
public void print(float f) {
trackContentLength(f);
this.delegate.print(f);
}
public void print(double d) {
trackContentLength(d);
this.delegate.print(d);
}
public void print(char[] s) {
trackContentLength(s);
this.delegate.print(s);
}
public void print(String s) {
trackContentLength(s);
this.delegate.print(s);
}
public void print(Object obj) {
trackContentLength(obj);
this.delegate.print(obj);
}
public void println() {
trackContentLengthLn();
this.delegate.println();
}
public void println(boolean x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(char x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(int x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(long x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(float x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(double x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(char[] x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(String x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public void println(Object x) {
trackContentLength(x);
trackContentLengthLn();
this.delegate.println(x);
}
public PrintWriter printf(String format, Object... args) {
return this.delegate.printf(format, args);
}
public PrintWriter printf(Locale l, String format, Object... args) {
return this.delegate.printf(l, format, args);
}
public PrintWriter format(String format, Object... args) {
return this.delegate.format(format, args);
}
public PrintWriter format(Locale l, String format, Object... args) {
return this.delegate.format(l, format, args);
}
public PrintWriter append(CharSequence csq) {
checkContentLength(csq.length());
return this.delegate.append(csq);
}
public PrintWriter append(CharSequence csq, int start, int end) {
checkContentLength(end - start);
return this.delegate.append(csq, start, end);
}
public PrintWriter append(char c) {
trackContentLength(c);
return this.delegate.append(c);
}
}
/**
* Ensures{@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked before
* calling methods that commit the response. We delegate all methods to the original
* {@link javax.servlet.ServletOutputStream} to ensure that the behavior is as close
* to the original {@link javax.servlet.ServletOutputStream} as possible. See SEC-2039
*
* @author Rob Winch
*/
private class SaveContextServletOutputStream extends ServletOutputStream {
private final ServletOutputStream delegate;
SaveContextServletOutputStream(ServletOutputStream delegate) {
this.delegate = delegate;
}
public void write(int b) throws IOException {
trackContentLength(b);
this.delegate.write(b);
}
public void flush() throws IOException {
doOnResponseCommitted();
this.delegate.flush();
}
public void close() throws IOException {
doOnResponseCommitted();
this.delegate.close();
}
public int hashCode() {
return this.delegate.hashCode();
}
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
public void print(boolean b) throws IOException {
trackContentLength(b);
this.delegate.print(b);
}
public void print(char c) throws IOException {
trackContentLength(c);
this.delegate.print(c);
}
public void print(double d) throws IOException {
trackContentLength(d);
this.delegate.print(d);
}
public void print(float f) throws IOException {
trackContentLength(f);
this.delegate.print(f);
}
public void print(int i) throws IOException {
trackContentLength(i);
this.delegate.print(i);
}
public void print(long l) throws IOException {
trackContentLength(l);
this.delegate.print(l);
}
public void print(String s) throws IOException {
trackContentLength(s);
this.delegate.print(s);
}
public void println() throws IOException {
trackContentLengthLn();
this.delegate.println();
}
public void println(boolean b) throws IOException {
trackContentLength(b);
trackContentLengthLn();
this.delegate.println(b);
}
public void println(char c) throws IOException {
trackContentLength(c);
trackContentLengthLn();
this.delegate.println(c);
}
public void println(double d) throws IOException {
trackContentLength(d);
trackContentLengthLn();
this.delegate.println(d);
}
public void println(float f) throws IOException {
trackContentLength(f);
trackContentLengthLn();
this.delegate.println(f);
}
public void println(int i) throws IOException {
trackContentLength(i);
trackContentLengthLn();
this.delegate.println(i);
}
public void println(long l) throws IOException {
trackContentLength(l);
trackContentLengthLn();
this.delegate.println(l);
}
public void println(String s) throws IOException {
trackContentLength(s);
trackContentLengthLn();
this.delegate.println(s);
}
public void write(byte[] b) throws IOException {
trackContentLength(b);
this.delegate.write(b);
}
public void write(byte[] b, int off, int len) throws IOException {
checkContentLength(len);
this.delegate.write(b, off, len);
}
public String toString() {
return getClass().getName() + "[delegate=" + this.delegate.toString() + "]";
}
@Override
public boolean isReady() {
return this.delegate.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener) {
this.delegate.setWriteListener(writeListener);
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.web.http;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Allows for easily ensuring that a request is only invoked once per request. This is a
* simplified version of spring-web's OncePerRequestFilter and copied to reduce the foot
* print required to use the session support.
*
* @author Rob Winch
* @since 1.0
*/
abstract class OncePerRequestFilter implements Filter {
/**
* Suffix that gets appended to the filter name for the "already filtered" request
* attribute.
*/
public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
private String alreadyFilteredAttributeName = getClass().getName()
.concat(ALREADY_FILTERED_SUFFIX);
/**
* This {@code doFilter} implementation stores a request attribute for
* "already filtered", proceeding without filtering again if the attribute is already
* there.
* @param request the request
* @param response the response
* @param filterChain the filter chain
* @throws ServletException if request is not HTTP request
* @throws IOException in case of I/O operation exception
*/
public final void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!(request instanceof HttpServletRequest)
|| !(response instanceof HttpServletResponse)) {
throw new ServletException(
"OncePerRequestFilter just supports HTTP requests");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
boolean hasAlreadyFilteredAttribute = request
.getAttribute(this.alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
// Do invoke this filter...
request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(this.alreadyFilteredAttributeName);
}
}
}
/**
* Same contract as for {@code doFilter}, but guaranteed to be just invoked once per
* request within a single request thread.
* <p>
* Provides HttpServletRequest and HttpServletResponse arguments instead of the
* default ServletRequest and ServletResponse ones.
*
* @param request the request
* @param response the response
* @param filterChain the FilterChain
* @throws ServletException thrown when a non-I/O exception has occurred
* @throws IOException thrown when an I/O exception of some sort has occurred
* @see Filter#doFilter
*/
protected abstract void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
public void init(FilterConfig config) {
}
public void destroy() {
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.web.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Allows customizing the {@link HttpServletRequest} and/or the
* {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.0
*/
public interface RequestResponsePostProcessor {
/**
* Allows customizing the {@link HttpServletRequest}.
*
* @param request the original {@link HttpServletRequest}. Cannot be null.
* @param response the original {@link HttpServletResponse}. This is NOT the result of
* {@link #wrapResponse(HttpServletRequest, HttpServletResponse)} Cannot be null. .
* @return a non-null {@link HttpServletRequest}
*/
HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response);
/**
* Allows customizing the {@link HttpServletResponse}.
*
* @param request the original {@link HttpServletRequest}. This is NOT the result of
* {@link #wrapRequest(HttpServletRequest, HttpServletResponse)}. Cannot be null.
* @param response the original {@link HttpServletResponse}. Cannot be null.
* @return a non-null {@link HttpServletResponse}
*/
HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2014-2017 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.web.http;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.springframework.context.ApplicationListener;
import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.web.context.ServletContextAware;
/**
* Receives {@link SessionDestroyedEvent} and {@link SessionCreatedEvent} and translates
* them into {@link HttpSessionEvent} and submits the {@link HttpSessionEvent} to every
* registered {@link HttpSessionListener}.
*
* @author Rob Winch
* @since 1.1
*/
public class SessionEventHttpSessionListenerAdapter
implements ApplicationListener<AbstractSessionEvent>, ServletContextAware {
private final List<HttpSessionListener> listeners;
private ServletContext context;
public SessionEventHttpSessionListenerAdapter(List<HttpSessionListener> listeners) {
super();
this.listeners = listeners;
}
/*
* (non-Javadoc)
*
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.
* springframework.context.ApplicationEvent)
*/
public void onApplicationEvent(AbstractSessionEvent event) {
if (this.listeners.isEmpty()) {
return;
}
HttpSessionEvent httpSessionEvent = createHttpSessionEvent(event);
for (HttpSessionListener listener : this.listeners) {
if (event instanceof SessionDestroyedEvent) {
listener.sessionDestroyed(httpSessionEvent);
}
else if (event instanceof SessionCreatedEvent) {
listener.sessionCreated(httpSessionEvent);
}
}
}
private HttpSessionEvent createHttpSessionEvent(AbstractSessionEvent event) {
Session session = event.getSession();
HttpSession httpSession = new HttpSessionAdapter<>(session,
this.context);
HttpSessionEvent httpSessionEvent = new HttpSessionEvent(httpSession);
return httpSessionEvent;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet
* .ServletContext)
*/
public void setServletContext(ServletContext servletContext) {
this.context = servletContext;
}
}

View File

@@ -0,0 +1,459 @@
/*
* Copyright 2014-2017 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.web.http;
import java.io.IOException;
import java.time.Instant;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.Order;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* Switches the {@link javax.servlet.http.HttpSession} implementation to be backed by a
* {@link org.springframework.session.Session}.
*
* The {@link SessionRepositoryFilter} wraps the
* {@link javax.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link javax.servlet.http.HttpSession} to be backed by a
* {@link org.springframework.session.Session} returned by the
* {@link org.springframework.session.SessionRepository}.
*
* The {@link SessionRepositoryFilter} uses a {@link HttpSessionStrategy} (default
* {@link CookieHttpSessionStrategy} to bridge logic between an
* {@link javax.servlet.http.HttpSession} and the
* {@link org.springframework.session.Session} abstraction. Specifically:
*
* <ul>
* <li>The session id is looked up using
* {@link HttpSessionStrategy#getRequestedSessionId(javax.servlet.http.HttpServletRequest)}
* . The default is to look in a cookie named SESSION.</li>
* <li>The session id of newly created {@link org.springframework.session.Session}
* is sent to the client using
* <li>The client is notified that the session id is no longer valid with
* {@link HttpSessionStrategy#onInvalidateSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
* </li>
* </ul>
*
* <p>
* The SessionRepositoryFilter must be placed before any Filter that access the
* HttpSession or that might commit the response to ensure the session is overridden and
* persisted properly.
* </p>
*
* @param <S> the {@link Session} type.
* @since 1.0
* @author Rob Winch
*/
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session>
extends OncePerRequestFilter {
private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class
.getName().concat(".SESSION_LOGGER");
private static final Log SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);
/**
* The session repository request attribute name.
*/
public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
.getName();
/**
* Invalid session id (not backed by the session repository) request attribute name.
*/
public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR
+ ".invalidSessionId";
private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
+ ".CURRENT_SESSION";
/**
* The default filter order.
*/
public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;
private final SessionRepository<S> sessionRepository;
private ServletContext servletContext;
private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
/**
* Creates a new instance.
*
* @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
*/
public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
if (sessionRepository == null) {
throw new IllegalArgumentException("sessionRepository cannot be null");
}
this.sessionRepository = sessionRepository;
}
/**
* Sets the {@link HttpSessionStrategy} to be used. The default is a
* {@link CookieHttpSessionStrategy}.
*
* @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null.
*/
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
if (httpSessionStrategy == null) {
throw new IllegalArgumentException("httpSessionStrategy cannot be null");
}
this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(
httpSessionStrategy);
}
/**
* Sets the {@link MultiHttpSessionStrategy} to be used. The default is a
* {@link CookieHttpSessionStrategy}.
*
* @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be
* null.
*/
public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
if (httpSessionStrategy == null) {
throw new IllegalArgumentException("httpSessionStrategy cannot be null");
}
this.httpSessionStrategy = httpSessionStrategy;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
wrappedRequest.commitSession();
}
}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
* Allows ensuring that the session is saved if the response is committed.
*
* @author Rob Winch
* @since 1.0
*/
private final class SessionRepositoryResponseWrapper
extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
/**
* Create a new {@link SessionRepositoryResponseWrapper}.
* @param request the request to be wrapped
* @param response the response to be wrapped
*/
SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
}
/**
* A {@link javax.servlet.http.HttpServletRequest} that retrieves the
* {@link javax.servlet.http.HttpSession} using a
* {@link org.springframework.session.SessionRepository}.
*
* @author Rob Winch
* @since 1.0
*/
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
/**
* Uses the HttpSessionStrategy to write the session id to the response and
* persist the Session.
*/
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionStrategy
.onInvalidateSession(this, this.response);
}
}
else {
S session = wrappedSession.getSession();
SessionRepositoryFilter.this.sessionRepository.save(session);
if (!isRequestedSessionIdValid()
|| !session.getId().equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
this, this.response);
}
}
}
@SuppressWarnings("unchecked")
private HttpSessionWrapper getCurrentSession() {
return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
}
private void setCurrentSession(HttpSessionWrapper currentSession) {
if (currentSession == null) {
removeAttribute(CURRENT_SESSION_ATTR);
}
else {
setAttribute(CURRENT_SESSION_ATTR, currentSession);
}
}
@SuppressWarnings("unused")
public String changeSessionId() {
HttpSession session = getSession(false);
if (session == null) {
throw new IllegalStateException(
"Cannot change session ID. There is no session associated with this request.");
}
// eagerly get session attributes in case implementation lazily loads them
Map<String, Object> attrs = new HashMap<>();
Enumeration<String> iAttrNames = session.getAttributeNames();
while (iAttrNames.hasMoreElements()) {
String attrName = iAttrNames.nextElement();
Object value = session.getAttribute(attrName);
attrs.put(attrName, value);
}
SessionRepositoryFilter.this.sessionRepository.delete(session.getId());
HttpSessionWrapper original = getCurrentSession();
setCurrentSession(null);
HttpSessionWrapper newSession = getSession();
original.setSession(newSession.getSession());
newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
for (Map.Entry<String, Object> attr : attrs.entrySet()) {
String attrName = attr.getKey();
Object attrValue = attr.getValue();
newSession.setAttribute(attrName, attrValue);
}
return newSession.getId();
}
@Override
public boolean isRequestedSessionIdValid() {
if (this.requestedSessionIdValid == null) {
String sessionId = getRequestedSessionId();
S session = sessionId == null ? null : getSession(sessionId);
return isRequestedSessionIdValid(session);
}
return this.requestedSessionIdValid;
}
private boolean isRequestedSessionIdValid(S session) {
if (this.requestedSessionIdValid == null) {
this.requestedSessionIdValid = session != null;
}
return this.requestedSessionIdValid;
}
private boolean isInvalidateClientSession() {
return getCurrentSession() == null && this.requestedSessionInvalidated;
}
private S getSession(String sessionId) {
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
session.setLastAccessedTime(Instant.now());
return session;
}
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
String requestedSessionId = getRequestedSessionId();
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
@Override
public ServletContext getServletContext() {
if (this.servletContext != null) {
return this.servletContext;
}
// Servlet 3.0+
return super.getServletContext();
}
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
public String getRequestedSessionId() {
return SessionRepositoryFilter.this.httpSessionStrategy
.getRequestedSessionId(this);
}
/**
* Allows creating an HttpSession from a Session instance.
*
* @author Rob Winch
* @since 1.0
*/
private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
HttpSessionWrapper(S session, ServletContext servletContext) {
super(session, servletContext);
}
@Override
public void invalidate() {
super.invalidate();
SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
setCurrentSession(null);
SessionRepositoryFilter.this.sessionRepository.delete(getId());
}
}
}
/**
* A delegating implementation of {@link MultiHttpSessionStrategy}.
*/
static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
private HttpSessionStrategy delegate;
/**
* Create a new {@link MultiHttpSessionStrategyAdapter} instance.
* @param delegate the delegate HTTP session strategy
*/
MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
this.delegate = delegate;
}
public String getRequestedSessionId(HttpServletRequest request) {
return this.delegate.getRequestedSessionId(request);
}
public void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response) {
this.delegate.onNewSession(session, request, response);
}
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
this.delegate.onInvalidateSession(request, response);
}
public HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response) {
return request;
}
public HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response) {
return response;
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2014-2017 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.web.socket.config.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.socket.handler.WebSocketConnectHandlerDecoratorFactory;
import org.springframework.session.web.socket.handler.WebSocketRegistryListener;
import org.springframework.session.web.socket.server.SessionRepositoryMessageInterceptor;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.StompWebSocketEndpointRegistration;
import org.springframework.web.socket.config.annotation.WebMvcStompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UrlPathHelper;
/**
* Eases configuration of Web Socket and Spring Session integration.
*
* <p>
* The configuration:
* </p>
* <ul>
* <li>Ensures the {@link Session} is kept alive on incoming web socket messages.</li>
* <li>Ensures that Web Socket Sessions are destroyed when a {@link Session} is terminated
* </li>
* </ul>
*
* <p>
* Example usage
* </p>
*
* <code>
* {@literal @Configuration}
* {@literal @EnableScheduling}
* {@literal @EnableWebSocketMessageBroker}
* {@literal public class WebSocketConfig<S extends Session> extends AbstractSessionWebSocketMessageBrokerConfigurer<S>} {
*
* {@literal @Override}
* protected void configureStompEndpoints(StompEndpointRegistry registry) {
* registry.addEndpoint("/messages")
* .withSockJS();
* }
*
* {@literal @Override}
* public void configureMessageBroker(MessageBrokerRegistry registry) {
* registry.enableSimpleBroker("/queue/", "/topic/");
* registry.setApplicationDestinationPrefixes("/app");
* }
* }
* </code>
*
* @param <S> the type of Session
* @author Rob Winch
* @since 1.0
*/
public abstract class AbstractSessionWebSocketMessageBrokerConfigurer<S extends Session>
extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
@SuppressWarnings("rawtypes")
private SessionRepository sessionRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(sessionRepositoryInterceptor());
}
public final void registerStompEndpoints(StompEndpointRegistry registry) {
if (registry instanceof WebMvcStompEndpointRegistry) {
WebMvcStompEndpointRegistry mvcRegistry = (WebMvcStompEndpointRegistry) registry;
configureStompEndpoints(new SessionStompEndpointRegistry(mvcRegistry,
sessionRepositoryInterceptor()));
}
}
/**
* Register STOMP endpoints mapping each to a specific URL and (optionally) enabling
* and configuring SockJS fallback options with a
* {@link SessionRepositoryMessageInterceptor} automatically added as an interceptor.
*
* @param registry the {@link StompEndpointRegistry} which automatically has a
* {@link SessionRepositoryMessageInterceptor} added to it.
*/
protected abstract void configureStompEndpoints(StompEndpointRegistry registry);
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(wsConnectHandlerDecoratorFactory());
}
@Bean
public WebSocketRegistryListener webSocketRegistryListener() {
return new WebSocketRegistryListener();
}
@Bean
public WebSocketConnectHandlerDecoratorFactory wsConnectHandlerDecoratorFactory() {
return new WebSocketConnectHandlerDecoratorFactory(this.eventPublisher);
}
@Bean
@SuppressWarnings("unchecked")
public SessionRepositoryMessageInterceptor<S> sessionRepositoryInterceptor() {
return new SessionRepositoryMessageInterceptor<>(this.sessionRepository);
}
/**
* A {@link StompEndpointRegistry} that applies {@link HandshakeInterceptor}.
*/
static class SessionStompEndpointRegistry implements StompEndpointRegistry {
private final WebMvcStompEndpointRegistry registry;
private final HandshakeInterceptor interceptor;
SessionStompEndpointRegistry(WebMvcStompEndpointRegistry registry,
HandshakeInterceptor interceptor) {
this.registry = registry;
this.interceptor = interceptor;
}
public StompWebSocketEndpointRegistration addEndpoint(String... paths) {
StompWebSocketEndpointRegistration endpoints = this.registry
.addEndpoint(paths);
endpoints.addInterceptors(this.interceptor);
return endpoints;
}
public void setOrder(int order) {
this.registry.setOrder(order);
}
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
this.registry.setUrlPathHelper(urlPathHelper);
}
public WebMvcStompEndpointRegistry setErrorHandler(
StompSubProtocolErrorHandler errorHandler) {
return this.registry.setErrorHandler(errorHandler);
}
}
}

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 org.springframework.session.web.socket.events;
import org.springframework.context.ApplicationEvent;
import org.springframework.session.web.socket.handler.WebSocketConnectHandlerDecoratorFactory;
import org.springframework.session.web.socket.handler.WebSocketRegistryListener;
import org.springframework.web.socket.WebSocketSession;
/**
* Similar to Spring {@link org.springframework.web.socket.messaging.SessionConnectEvent}
* except that it provides access to the {@link WebSocketSession} to allow mapping the
* Spring Session to the {@link WebSocketSession}.
*
* @author Rob Winch
* @since 1.0
* @see WebSocketRegistryListener
* @see WebSocketConnectHandlerDecoratorFactory
*/
@SuppressWarnings("serial")
public class SessionConnectEvent extends ApplicationEvent {
private final WebSocketSession webSocketSession;
public SessionConnectEvent(Object source, WebSocketSession webSocketSession) {
super(source);
this.webSocketSession = webSocketSession;
}
public WebSocketSession getWebSocketSession() {
return this.webSocketSession;
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.web.socket.handler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.session.Session;
import org.springframework.session.web.socket.events.SessionConnectEvent;
import org.springframework.util.Assert;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
/**
* Ensures that a {@link SessionConnectEvent} is published in
* {@link WebSocketHandler#afterConnectionEstablished(WebSocketSession)}. This is
* necessary so that the {@link WebSocketSession} can be mapped to the corresponding
* Spring {@link Session} to terminate any {@link WebSocketSession} associated with a
* Spring {@link Session} that was destroyed.
*
* @author Rob Winch
* @since 1.0
*
* @see WebSocketRegistryListener
*/
public final class WebSocketConnectHandlerDecoratorFactory
implements WebSocketHandlerDecoratorFactory {
private static final Log logger = LogFactory
.getLog(WebSocketConnectHandlerDecoratorFactory.class);
private final ApplicationEventPublisher eventPublisher;
/**
* Creates a new instance.
*
* @param eventPublisher the {@link ApplicationEventPublisher} to use. Cannot be null.
*/
public WebSocketConnectHandlerDecoratorFactory(
ApplicationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
public WebSocketHandler decorate(WebSocketHandler handler) {
return new SessionWebSocketHandler(handler);
}
private final class SessionWebSocketHandler extends WebSocketHandlerDecorator {
SessionWebSocketHandler(WebSocketHandler delegate) {
super(delegate);
}
@Override
public void afterConnectionEstablished(WebSocketSession wsSession)
throws Exception {
super.afterConnectionEstablished(wsSession);
publishEvent(new SessionConnectEvent(this, wsSession));
}
private void publishEvent(ApplicationEvent event) {
try {
WebSocketConnectHandlerDecoratorFactory.this.eventPublisher
.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2014-2017 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.web.socket.handler;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.web.socket.events.SessionConnectEvent;
import org.springframework.session.web.socket.server.SessionRepositoryMessageInterceptor;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
/**
* <p>
* Keeps track of mapping the Spring Session ID to the {@link WebSocketSession} and
* ensuring when a {@link SessionDestroyedEvent} is fired that the
* {@link WebSocketSession} is closed.
* </p>
*
*
* @author Rob Winch
* @author Mark Anderson
* @since 1.0
*/
public final class WebSocketRegistryListener
implements ApplicationListener<ApplicationEvent> {
private static final Log logger = LogFactory.getLog(WebSocketRegistryListener.class);
static final CloseStatus SESSION_EXPIRED_STATUS = new CloseStatus(
CloseStatus.POLICY_VIOLATION.getCode(),
"This connection was established under an authenticated HTTP Session that has expired");
private final ConcurrentHashMap<String, Map<String, WebSocketSession>> httpSessionIdToWsSessions = new ConcurrentHashMap<>();
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof SessionDestroyedEvent) {
SessionDestroyedEvent e = (SessionDestroyedEvent) event;
closeWsSessions(e.getSessionId());
}
else if (event instanceof SessionConnectEvent) {
SessionConnectEvent e = (SessionConnectEvent) event;
afterConnectionEstablished(e.getWebSocketSession());
}
else if (event instanceof SessionDisconnectEvent) {
SessionDisconnectEvent e = (SessionDisconnectEvent) event;
Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor
.getSessionAttributes(e.getMessage().getHeaders());
String httpSessionId = sessionAttributes == null ? null
: SessionRepositoryMessageInterceptor.getSessionId(sessionAttributes);
afterConnectionClosed(httpSessionId, e.getSessionId());
}
}
private void afterConnectionEstablished(WebSocketSession wsSession) {
Principal principal = wsSession.getPrincipal();
if (principal == null) {
return;
}
String httpSessionId = getHttpSessionId(wsSession);
registerWsSession(httpSessionId, wsSession);
}
private String getHttpSessionId(WebSocketSession wsSession) {
Map<String, Object> attributes = wsSession.getAttributes();
return SessionRepositoryMessageInterceptor.getSessionId(attributes);
}
private void afterConnectionClosed(String httpSessionId, String wsSessionId) {
if (httpSessionId == null) {
return;
}
Map<String, WebSocketSession> sessions = this.httpSessionIdToWsSessions
.get(httpSessionId);
if (sessions != null) {
boolean result = sessions.remove(wsSessionId) != null;
if (logger.isDebugEnabled()) {
logger.debug("Removal of " + wsSessionId + " was " + result);
}
if (sessions.isEmpty()) {
this.httpSessionIdToWsSessions.remove(httpSessionId);
if (logger.isDebugEnabled()) {
logger.debug("Removed the corresponding HTTP Session for "
+ wsSessionId + " since it contained no WebSocket mappings");
}
}
}
}
private void registerWsSession(String httpSessionId, WebSocketSession wsSession) {
Map<String, WebSocketSession> sessions = this.httpSessionIdToWsSessions
.get(httpSessionId);
if (sessions == null) {
sessions = new ConcurrentHashMap<>();
this.httpSessionIdToWsSessions.putIfAbsent(httpSessionId, sessions);
sessions = this.httpSessionIdToWsSessions.get(httpSessionId);
}
sessions.put(wsSession.getId(), wsSession);
}
private void closeWsSessions(String httpSessionId) {
Map<String, WebSocketSession> sessionsToClose = this.httpSessionIdToWsSessions
.remove(httpSessionId);
if (sessionsToClose == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug(
"Closing WebSocket connections associated to expired HTTP Session "
+ httpSessionId);
}
for (WebSocketSession toClose : sessionsToClose.values()) {
try {
toClose.close(SESSION_EXPIRED_STATUS);
}
catch (IOException e) {
logger.debug(
"Failed to close WebSocketSession (this is nothing to worry about but for debugging only)",
e);
}
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2014-2017 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.web.socket.server;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
/**
* <p>
* Acts as a {@link ChannelInterceptor} and a {@link HandshakeInterceptor} to ensure the
* {@link Session#getLastAccessedTime()} is up to date.
* </p>
* <ul>
* <li>Associates the {@link Session#getId()} with the WebSocket Session attributes when
* the handshake is performed. This is later used when intercepting messages to ensure the
* {@link Session#getLastAccessedTime()} is updated.</li>
* <li>Intercepts {@link Message}'s that are have {@link SimpMessageType} that corresponds
* to {@link #setMatchingMessageTypes(Set)} and updates the last accessed time of the
* {@link Session}. If the {@link Session} is expired, the {@link Message} is prevented
* from proceeding.</li>
* </ul>
*
* <p>
* In order to work {@link SessionRepositoryMessageInterceptor} must be registered as a
* {@link ChannelInterceptor} and a {@link HandshakeInterceptor} .
* </p>
*
* @param <S> the {@link Session} type
* @author Rob Winch
* @since 1.0
*/
public final class SessionRepositoryMessageInterceptor<S extends Session>
extends ChannelInterceptorAdapter implements HandshakeInterceptor {
private static final String SPRING_SESSION_ID_ATTR_NAME = "SPRING.SESSION.ID";
private final SessionRepository<S> sessionRepository;
private Set<SimpMessageType> matchingMessageTypes;
/**
* Creates a new instance.
*
* @param sessionRepository the {@link SessionRepository} to use. Cannot be null.
*/
public SessionRepositoryMessageInterceptor(SessionRepository<S> sessionRepository) {
Assert.notNull(sessionRepository, "sessionRepository cannot be null");
this.sessionRepository = sessionRepository;
this.matchingMessageTypes = EnumSet.of(SimpMessageType.CONNECT,
SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE,
SimpMessageType.UNSUBSCRIBE);
}
/**
* <p>
* Sets the {@link SimpMessageType} to match on. If the {@link Message} matches, then
* {@link #preSend(Message, MessageChannel)} ensures the {@link Session} is not
* expired and updates the {@link Session#getLastAccessedTime()}
* </p>
*
* <p>
* The default is: SimpMessageType.CONNECT, SimpMessageType.MESSAGE,
* SimpMessageType.SUBSCRIBE, SimpMessageType.UNSUBSCRIBE.
* </p>
*
* @param matchingMessageTypes the {@link SimpMessageType} to match on in
* {@link #preSend(Message, MessageChannel)}, else the {@link Message} is continued
* without accessing or updating the {@link Session}
*/
public void setMatchingMessageTypes(Set<SimpMessageType> matchingMessageTypes) {
Assert.notEmpty(matchingMessageTypes,
"matchingMessageTypes cannot be null or empty");
this.matchingMessageTypes = matchingMessageTypes;
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if (message == null) {
return message;
}
SimpMessageType messageType = SimpMessageHeaderAccessor
.getMessageType(message.getHeaders());
if (!this.matchingMessageTypes.contains(messageType)) {
return super.preSend(message, channel);
}
Map<String, Object> sessionHeaders = SimpMessageHeaderAccessor
.getSessionAttributes(message.getHeaders());
String sessionId = sessionHeaders == null ? null
: (String) sessionHeaders.get(SPRING_SESSION_ID_ATTR_NAME);
if (sessionId != null) {
S session = this.sessionRepository.getSession(sessionId);
if (session != null) {
// update the last accessed time
session.setLastAccessedTime(Instant.now());
this.sessionRepository.save(session);
}
}
return super.preSend(message, channel);
}
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
setSessionId(attributes, session.getId());
}
}
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
public static String getSessionId(Map<String, Object> attributes) {
return (String) attributes.get(SPRING_SESSION_ID_ATTR_NAME);
}
public static void setSessionId(Map<String, Object> attributes, String sessionId) {
attributes.put(SPRING_SESSION_ID_ATTR_NAME, sessionId);
}
}

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES;
DROP TABLE SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES;
DROP TABLE SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS SPRING_SESSION_ATTRIBUTES;
DROP TABLE IF EXISTS SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES IF EXISTS;
DROP TABLE SPRING_SESSION IF EXISTS;

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS SPRING_SESSION_ATTRIBUTES;
DROP TABLE IF EXISTS SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES;
DROP TABLE SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS SPRING_SESSION_ATTRIBUTES;
DROP TABLE IF EXISTS SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS SPRING_SESSION_ATTRIBUTES;
DROP TABLE IF EXISTS SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES;
DROP TABLE SPRING_SESSION;

View File

@@ -0,0 +1,2 @@
DROP TABLE SPRING_SESSION_ATTRIBUTES;
DROP TABLE SPRING_SESSION;

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
) ENGINE=InnoDB;
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME NUMBER(19,0) NOT NULL,
LAST_ACCESS_TIME NUMBER(19,0) NOT NULL,
MAX_INACTIVE_INTERVAL NUMBER(10,0) NOT NULL,
PRINCIPAL_NAME VARCHAR2(100 CHAR),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR2(200 CHAR) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BYTEA NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHARACTER(36) NOT NULL,
CREATION_TIME INTEGER NOT NULL,
LAST_ACCESS_TIME INTEGER NOT NULL,
MAX_INACTIVE_INTERVAL INTEGER NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES IMAGE NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,20 @@
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
) LOCK DATAROWS;
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES IMAGE NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
) LOCK DATAROWS;
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2014-2017 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;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class MapSessionRepositoryTests {
MapSessionRepository repository;
MapSession session;
@Before
public void setup() {
this.repository = new MapSessionRepository();
this.session = new MapSession();
}
@Test
public void getSessionExpired() {
this.session.setMaxInactiveInterval(Duration.ofSeconds(1));
this.session.setLastAccessedTime(Instant.now().minus(5, ChronoUnit.MINUTES));
this.repository.save(this.session);
assertThat(this.repository.getSession(this.session.getId())).isNull();
}
@Test
public void createSessionDefaultExpiration() {
Session session = this.repository.createSession();
assertThat(session).isInstanceOf(MapSession.class);
assertThat(session.getMaxInactiveInterval())
.isEqualTo(new MapSession().getMaxInactiveInterval());
}
@Test
public void createSessionCustomDefaultExpiration() {
final Duration expectedMaxInterval = new MapSession().getMaxInactiveInterval()
.plusSeconds(10);
this.repository.setDefaultMaxInactiveInterval(
(int) expectedMaxInterval.getSeconds());
Session session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval())
.isEqualTo(expectedMaxInterval);
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2014-2017 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;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class MapSessionTests {
private MapSession session;
@Before
public void setup() {
this.session = new MapSession();
this.session.setLastAccessedTime(Instant.ofEpochMilli(1413258262962L));
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullSession() {
new MapSession((Session) null);
}
/**
* Ensure conforms to the javadoc of {@link Session}
*/
@Test
public void setAttributeNullObjectRemoves() {
String attr = "attr";
this.session.setAttribute(attr, new Object());
this.session.setAttribute(attr, null);
assertThat(this.session.getAttributeNames()).isEmpty();
}
@Test
public void equalsNonSessionFalse() {
assertThat(this.session.equals(new Object())).isFalse();
}
@Test
public void equalsCustomSession() {
CustomSession other = new CustomSession();
this.session.setId(other.getId());
assertThat(this.session.equals(other)).isTrue();
}
@Test
public void hashCodeEqualsIdHashCode() {
this.session.setId("constantId");
assertThat(this.session.hashCode()).isEqualTo(this.session.getId().hashCode());
}
@Test
public void isExpiredExact() {
Instant now = Instant.ofEpochMilli(1413260062962L);
assertThat(this.session.isExpired(now)).isTrue();
}
@Test
public void isExpiredOneMsTooSoon() {
Instant now = Instant.ofEpochMilli(1413260062961L);
assertThat(this.session.isExpired(now)).isFalse();
}
@Test
public void isExpiredOneMsAfter() {
Instant now = Instant.ofEpochMilli(1413260062963L);
assertThat(this.session.isExpired(now)).isTrue();
}
static class CustomSession implements Session {
public Instant getCreationTime() {
return Instant.EPOCH;
}
public String getId() {
return "id";
}
public void setLastAccessedTime(Instant lastAccessedTime) {
throw new UnsupportedOperationException();
}
public Instant getLastAccessedTime() {
return Instant.EPOCH;
}
public void setMaxInactiveInterval(Duration interval) {
}
public Duration getMaxInactiveInterval() {
return Duration.ZERO;
}
public <T> Optional<T> getAttribute(String attributeName) {
return Optional.empty();
}
public Set<String> getAttributeNames() {
return null;
}
public void setAttribute(String attributeName, Object attributeValue) {
}
public void removeAttribute(String attributeName) {
}
public boolean isExpired() {
return false;
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2014-2017 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.config.annotation.web.http;
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
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.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* @author Rob Winch
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class EnableSpringHttpSessionCustomCookieSerializerTests {
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
@Autowired
CookieSerializer cookieSerializer;
@Before
public void setup() {
this.chain = new MockFilterChain();
}
@Test
public void usesReadSessionIds() throws Exception {
String sessionId = "sessionId";
given(this.cookieSerializer.readCookieValues(any(HttpServletRequest.class)))
.willReturn(Arrays.asList(sessionId));
this.sessionRepositoryFilter.doFilter(this.request, this.response, this.chain);
assertThat(getRequest().getRequestedSessionId()).isEqualTo(sessionId);
}
@Test
public void usesWrite() throws Exception {
this.sessionRepositoryFilter.doFilter(this.request, this.response,
new MockFilterChain() {
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
((HttpServletRequest) request).getSession();
super.doFilter(request, response);
}
});
verify(this.cookieSerializer).writeCookieValue(any(CookieValue.class));
}
private HttpServletRequest getRequest() {
return (HttpServletRequest) this.chain.getRequest();
}
@EnableSpringHttpSession
@Configuration
static class Config {
@Bean
public MapSessionRepository mapSessionRepository() {
return new MapSessionRepository();
}
@Bean
public CookieSerializer cookieSerializer() {
return mock(CookieSerializer.class);
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2014-2017 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.config.annotation.web.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.web.http.MultiHttpSessionStrategy;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* @author Rob Winch
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class EnableSpringHttpSessionCustomMultiHttpSessionStrategyTests {
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
SessionRepositoryFilter<? extends Session> sessionRepositoryFilter;
@Autowired
MultiHttpSessionStrategy strategy;
@Before
public void setup() {
this.chain = new MockFilterChain();
}
@Test
public void wrapRequestAndResponseUsed() throws Exception {
given(this.strategy.wrapRequest(any(HttpServletRequest.class),
any(HttpServletResponse.class))).willReturn(this.request);
given(this.strategy.wrapResponse(any(HttpServletRequest.class),
any(HttpServletResponse.class))).willReturn(this.response);
this.sessionRepositoryFilter.doFilter(this.request, this.response, this.chain);
verify(this.strategy).wrapRequest(any(HttpServletRequest.class),
any(HttpServletResponse.class));
verify(this.strategy).wrapResponse(any(HttpServletRequest.class),
any(HttpServletResponse.class));
}
@EnableSpringHttpSession
@Configuration
static class Config {
@Bean
public MapSessionRepository mapSessionRepository() {
return new MapSessionRepository();
}
@Bean
public MultiHttpSessionStrategy strategy() {
return mock(MultiHttpSessionStrategy.class);
}
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2014-2017 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.config.annotation.web.http;
import javax.servlet.ServletContext;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
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.mock.web.MockServletContext;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.SessionEventHttpSessionListenerAdapter;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SpringHttpSessionConfiguration}.
*
* @author Vedran Pavic
*/
@RunWith(MockitoJUnitRunner.class)
public class SpringHttpSessionConfigurationTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
@Test
public void noSessionRepositoryConfiguration() {
this.thrown.expect(UnsatisfiedDependencyException.class);
this.thrown.expectMessage("org.springframework.session.SessionRepository");
registerAndRefresh(EmptyConfiguration.class);
}
@Test
public void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(SessionEventHttpSessionListenerAdapter.class))
.isNotNull();
assertThat(this.context.getBean(SessionRepositoryFilter.class)).isNotNull();
assertThat(this.context.getBean(SessionRepository.class)).isNotNull();
}
@Test
public void sessionCookieConfigConfiguration() {
registerAndRefresh(SessionCookieConfigConfiguration.class);
SessionRepositoryFilter sessionRepositoryFilter = this.context
.getBean(SessionRepositoryFilter.class);
assertThat(sessionRepositoryFilter).isNotNull();
CookieHttpSessionStrategy httpSessionStrategy = (CookieHttpSessionStrategy) ReflectionTestUtils
.getField(sessionRepositoryFilter, "httpSessionStrategy");
assertThat(httpSessionStrategy).isNotNull();
DefaultCookieSerializer cookieSerializer = (DefaultCookieSerializer) ReflectionTestUtils
.getField(httpSessionStrategy, "cookieSerializer");
assertThat(cookieSerializer).isNotNull();
assertThat(ReflectionTestUtils.getField(cookieSerializer, "cookieName"))
.isEqualTo("test-name");
assertThat(ReflectionTestUtils.getField(cookieSerializer, "cookiePath"))
.isEqualTo("test-path");
assertThat(ReflectionTestUtils.getField(cookieSerializer, "cookieMaxAge"))
.isEqualTo(600);
assertThat(ReflectionTestUtils.getField(cookieSerializer, "domainName"))
.isEqualTo("test-domain");
}
@Test
public void rememberMeServicesConfiguration() {
registerAndRefresh(RememberMeServicesConfiguration.class);
SessionRepositoryFilter sessionRepositoryFilter = this.context
.getBean(SessionRepositoryFilter.class);
assertThat(sessionRepositoryFilter).isNotNull();
CookieHttpSessionStrategy httpSessionStrategy = (CookieHttpSessionStrategy) ReflectionTestUtils
.getField(sessionRepositoryFilter, "httpSessionStrategy");
assertThat(httpSessionStrategy).isNotNull();
DefaultCookieSerializer cookieSerializer = (DefaultCookieSerializer) ReflectionTestUtils
.getField(httpSessionStrategy, "cookieSerializer");
assertThat(cookieSerializer).isNotNull();
assertThat(ReflectionTestUtils.getField(cookieSerializer,
"rememberMeRequestAttribute")).isEqualTo(
SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
}
@Configuration
@EnableSpringHttpSession
static class EmptyConfiguration {
}
static class BaseConfiguration {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository();
}
}
@Configuration
@EnableSpringHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
@EnableSpringHttpSession
static class SessionCookieConfigConfiguration extends BaseConfiguration {
@Bean
public ServletContext servletContext() {
MockServletContext servletContext = new MockServletContext();
servletContext.getSessionCookieConfig().setName("test-name");
servletContext.getSessionCookieConfig().setDomain("test-domain");
servletContext.getSessionCookieConfig().setPath("test-path");
servletContext.getSessionCookieConfig().setMaxAge(600);
return servletContext;
}
}
@Configuration
@EnableSpringHttpSession
static class RememberMeServicesConfiguration extends BaseConfiguration {
@Bean
public SpringSessionRememberMeServices rememberMeServices() {
return new SpringSessionRememberMeServices();
}
}
}

View File

@@ -0,0 +1,536 @@
/*
* Copyright 2014-2017 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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;
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 org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* 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;
@Mock
private PlatformTransactionManager transactionManager;
private JdbcOperationsSessionRepository repository;
@Before
public void setUp() {
this.repository = new JdbcOperationsSessionRepository(
this.jdbcOperations, this.transactionManager);
}
@Test
public void constructorDataSource() {
JdbcOperationsSessionRepository repository = new JdbcOperationsSessionRepository(
this.dataSource, this.transactionManager);
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, this.transactionManager);
}
@Test
public void constructorNullJdbcOperations() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("JdbcOperations must not be null");
new JdbcOperationsSessionRepository((JdbcOperations) null, this.transactionManager);
}
@Test
public void constructorNullTransactionManager() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Property 'transactionManager' is required");
new JdbcOperationsSessionRepository(this.jdbcOperations, null);
}
@Test
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 setCreateSessionQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setCreateSessionQuery(null);
}
@Test
public void setCreateSessionQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setCreateSessionQuery(" ");
}
@Test
public void setCreateSessionAttributeQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setCreateSessionAttributeQuery(null);
}
@Test
public void setCreateSessionAttributeQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setCreateSessionAttributeQuery(" ");
}
@Test
public void setGetSessionQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setGetSessionQuery(null);
}
@Test
public void setGetSessionQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setGetSessionQuery(" ");
}
@Test
public void setUpdateSessionQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setUpdateSessionQuery(null);
}
@Test
public void setUpdateSessionQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setUpdateSessionQuery(" ");
}
@Test
public void setUpdateSessionAttributeQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setUpdateSessionAttributeQuery(null);
}
@Test
public void setUpdateSessionAttributeQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setUpdateSessionAttributeQuery(" ");
}
@Test
public void setDeleteSessionAttributeQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionAttributeQuery(null);
}
@Test
public void setDeleteSessionAttributeQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionAttributeQuery(" ");
}
@Test
public void setDeleteSessionQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionQuery(null);
}
@Test
public void setDeleteSessionQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionQuery(" ");
}
@Test
public void setListSessionsByPrincipalNameQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setListSessionsByPrincipalNameQuery(null);
}
@Test
public void setListSessionsByPrincipalNameQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setListSessionsByPrincipalNameQuery(" ");
}
@Test
public void setDeleteSessionsByLastAccessTimeQueryNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionsByLastAccessTimeQuery(null);
}
@Test
public void setDeleteSessionsByLastAccessTimeQueryEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Query must not be empty");
this.repository.setDeleteSessionsByLastAccessTimeQuery(" ");
}
@Test
public void setLobHandlerNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LobHandler must not be null");
this.repository.setLobHandler(null);
}
@Test
public void setConversionServiceNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("conversionService must not be null");
this.repository.setConversionService(null);
}
@Test
public void createSessionDefaultMaxInactiveInterval() throws Exception {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveInterval())
.isEqualTo(new MapSession().getMaxInactiveInterval());
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.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(interval));
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void saveNewWithoutAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
public void saveNewWithAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
session.setAttribute("testName", "testValue");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"),
isA(PreparedStatementSetter.class));
verify(this.jdbcOperations, times(1)).batchUpdate(
and(startsWith("INSERT"), contains("ATTRIBUTE_BYTES")),
isA(BatchPreparedStatementSetter.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();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(
and(startsWith("UPDATE"), contains("ATTRIBUTE_BYTES")),
isA(PreparedStatementSetter.class));
}
@Test
public void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(
new MapSession());
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(
and(startsWith("UPDATE"), contains("LAST_ACCESS_TIME")),
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";
given(this.jdbcOperations.query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)))
.willReturn(Collections.emptyList());
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.getSession(sessionId);
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
}
@Test
public void getSessionExpired() {
MapSession expired = new MapSession();
expired.setLastAccessedTime(Instant.now().minusSeconds(
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
given(this.jdbcOperations.query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)))
.willReturn(Collections.singletonList(expired));
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.getSession(expired.getId());
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"),
eq(expired.getId()));
}
@Test
public void getSessionFound() {
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
given(this.jdbcOperations.query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)))
.willReturn(Collections.singletonList(saved));
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.getSession(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.isNew()).isFalse();
assertThat(session.<String>getAttribute("savedName").orElse(null)).isEqualTo("savedValue");
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
}
@Test
public void delete() {
String sessionId = "testSessionId";
this.repository.delete(sessionId);
assertPropagationRequiresNew();
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";
given(this.jdbcOperations.query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)))
.willReturn(Collections.emptyList());
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal);
assertThat(sessions).isEmpty();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal,
"notused", AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<>(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);
given(this.jdbcOperations.query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class)))
.willReturn(saved);
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal);
assertThat(sessions).hasSize(2);
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
}
@Test
public void cleanupExpiredSessions() {
this.repository.cleanUpExpiredSessions();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong());
}
private void assertPropagationRequiresNew() {
ArgumentCaptor<TransactionDefinition> argument =
ArgumentCaptor.forClass(TransactionDefinition.class);
verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture());
assertThat(argument.getValue().getPropagationBehavior())
.isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2014-2017 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.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
*
*/
public class JdbcHttpSessionConfigurationCustomCronTests {
AnnotationConfigApplicationContext context;
@Before
public void setup() {
this.context = new AnnotationConfigApplicationContext();
}
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void overrideCron() {
this.context.register(Config.class);
assertThatThrownBy(() ->
JdbcHttpSessionConfigurationCustomCronTests.this.context.refresh())
.hasStackTraceContaining(
"Encountered invalid @Scheduled method 'cleanUpExpiredSessions': Cron expression must consist of 6 fields (found 1 in \"oops\")");
}
@EnableJdbcHttpSession
@Configuration
@PropertySource("classpath:spring-session-cleanup-cron-expression-oops.properties")
static class Config {
@Bean
public DataSource dataSource() {
return mock(DataSource.class);
}
@Bean
public PlatformTransactionManager transactionManager() {
return mock(PlatformTransactionManager.class);
}
}
}

View File

@@ -0,0 +1,275 @@
/*
* 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.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.ConversionService;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JdbcHttpSessionConfiguration}.
*
* @author Vedran Pavic
* @author Eddú Meléndez
* @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 setCustomTableName() {
registerAndRefresh(BaseConfiguration.class,
CustomTableNameSetConfiguration.class);
JdbcHttpSessionConfiguration repository = this.context
.getBean(JdbcHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(
"custom_session");
}
@Test
public void setCustomMaxInactiveIntervalInSeconds() {
registerAndRefresh(BaseConfiguration.class,
CustomMaxInactiveIntervalInSecondsSetConfiguration.class);
JdbcHttpSessionConfiguration repository = this.context
.getBean(JdbcHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "maxInactiveIntervalInSeconds")).isEqualTo(
10);
}
@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
public void customConversionServiceConfiguration() {
registerAndRefresh(CustomConversionServiceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context
.getBean(JdbcOperationsSessionRepository.class);
ConversionService conversionService = this.context
.getBean("springSessionConversionService", ConversionService.class);
assertThat(repository).isNotNull();
assertThat(conversionService).isNotNull();
Object repositoryConversionService = ReflectionTestUtils.getField(repository,
"conversionService");
assertThat(repositoryConversionService).isEqualTo(conversionService);
}
@Test
public void resolveTableNameByPropertyPlaceholder() {
this.context.setEnvironment(new MockEnvironment().withProperty("session.jdbc.tableName", "custom_session_table"));
registerAndRefresh(CustomJdbcHttpSessionConfiguration.class);
JdbcHttpSessionConfiguration configuration = this.context.getBean(JdbcHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "tableName")).isEqualTo("custom_session_table");
}
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);
}
@Bean
public PlatformTransactionManager transactionManager() {
return mock(PlatformTransactionManager.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession(tableName = TABLE_NAME)
static class CustomTableNameConfiguration extends BaseConfiguration {
}
@Configuration
static class CustomTableNameSetConfiguration extends JdbcHttpSessionConfiguration {
CustomTableNameSetConfiguration() {
setTableName("custom_session");
}
}
@Configuration
static class CustomMaxInactiveIntervalInSecondsSetConfiguration extends JdbcHttpSessionConfiguration {
CustomMaxInactiveIntervalInSecondsSetConfiguration() {
setMaxInactiveIntervalInSeconds(10);
}
}
@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 CustomConversionServiceConfiguration extends BaseConfiguration {
@Bean
public ConversionService springSessionConversionService() {
return mock(ConversionService.class);
}
}
@Configuration
@EnableJdbcHttpSession(tableName = "${session.jdbc.tableName}")
static class CustomJdbcHttpSessionConfiguration extends BaseConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2014-2017 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.security;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.User;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.when;
/**
* Tests for {@link SpringSessionBackedSessionRegistry}.
*/
@RunWith(MockitoJUnitRunner.class)
public class SpringSessionBackedSessionRegistryTest {
private static final String SESSION_ID = "sessionId";
private static final String SESSION_ID2 = "otherSessionId";
private static final String USER_NAME = "userName";
private static final User PRINCIPAL = new User(USER_NAME, "password",
Collections.emptyList());
private static final Instant NOW = Instant.now();
@Mock
private FindByIndexNameSessionRepository<Session> sessionRepository;
@InjectMocks
private SpringSessionBackedSessionRegistry<Session> sessionRegistry;
@Test
public void sessionInformationForExistingSession() {
Session session = createSession(SESSION_ID, USER_NAME, NOW);
when(this.sessionRepository.getSession(SESSION_ID)).thenReturn(session);
SessionInformation sessionInfo = this.sessionRegistry
.getSessionInformation(SESSION_ID);
assertThat(sessionInfo.getSessionId()).isEqualTo(SESSION_ID);
assertThat(sessionInfo.getLastRequest().toInstant()).isEqualTo(NOW);
assertThat(sessionInfo.getPrincipal()).isEqualTo(USER_NAME);
assertThat(sessionInfo.isExpired()).isFalse();
}
@Test
public void sessionInformationForExpiredSession() {
Session session = createSession(SESSION_ID, USER_NAME, NOW);
session.setAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR,
Boolean.TRUE);
when(this.sessionRepository.getSession(SESSION_ID)).thenReturn(session);
SessionInformation sessionInfo = this.sessionRegistry
.getSessionInformation(SESSION_ID);
assertThat(sessionInfo.getSessionId()).isEqualTo(SESSION_ID);
assertThat(sessionInfo.getLastRequest().toInstant()).isEqualTo(NOW);
assertThat(sessionInfo.getPrincipal()).isEqualTo(USER_NAME);
assertThat(sessionInfo.isExpired()).isTrue();
}
@Test
public void noSessionInformationForMissingSession() {
assertThat(this.sessionRegistry.getSessionInformation("nonExistingSessionId"))
.isNull();
}
@Test
public void getAllSessions() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry
.getAllSessions(PRINCIPAL, true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID,
SESSION_ID2);
}
@Test
public void getNonExpiredSessions() {
setUpSessions();
List<SessionInformation> nonExpiredSessionInfos = this.sessionRegistry
.getAllSessions(PRINCIPAL, false);
assertThat(nonExpiredSessionInfos).extracting("sessionId")
.containsExactly(SESSION_ID2);
}
@Test
public void expireNow() {
Session session = createSession(SESSION_ID, USER_NAME, NOW);
when(this.sessionRepository.getSession(SESSION_ID)).thenReturn(session);
SessionInformation sessionInfo = this.sessionRegistry
.getSessionInformation(SESSION_ID);
assertThat(sessionInfo.isExpired()).isFalse();
sessionInfo.expireNow();
assertThat(sessionInfo.isExpired()).isTrue();
ArgumentCaptor<Session> captor = ArgumentCaptor.forClass(Session.class);
verify(this.sessionRepository).save(captor.capture());
assertThat(captor.getValue().<Boolean>getAttribute(
SpringSessionBackedSessionInformation.EXPIRED_ATTR))
.isEqualTo(Optional.of(Boolean.TRUE));
}
private Session createSession(String sessionId, String userName,
Instant lastAccessed) {
MapSession session = new MapSession(sessionId);
session.setLastAccessedTime(lastAccessed);
Authentication authentication = mock(Authentication.class);
when(authentication.getName()).thenReturn(userName);
SecurityContextImpl securityContext = new SecurityContextImpl();
securityContext.setAuthentication(authentication);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return session;
}
private void setUpSessions() {
Session session1 = createSession(SESSION_ID, USER_NAME, NOW);
session1.setAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR,
Boolean.TRUE);
Session session2 = createSession(SESSION_ID2, USER_NAME, NOW);
Map<String, Session> sessions = new LinkedHashMap<>();
sessions.put(session1.getId(), session1);
sessions.put(session2.getId(), session2);
when(this.sessionRepository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, USER_NAME))
.thenReturn(sessions);
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2014-2017 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.security.web.authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link SpringSessionRememberMeServices}.
*
* @author Vedran Pavic
*/
public class SpringSessionRememberMeServicesTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private SpringSessionRememberMeServices rememberMeServices;
@Test
public void create() {
this.rememberMeServices = new SpringSessionRememberMeServices();
assertThat(ReflectionTestUtils.getField(this.rememberMeServices,
"rememberMeParameterName")).isEqualTo("remember-me");
assertThat(
ReflectionTestUtils.getField(this.rememberMeServices, "alwaysRemember"))
.isEqualTo(false);
assertThat(
ReflectionTestUtils.getField(this.rememberMeServices, "validitySeconds"))
.isEqualTo(2592000);
}
@Test
public void createWithCustomParameter() {
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setRememberMeParameterName("test-param");
assertThat(ReflectionTestUtils.getField(this.rememberMeServices,
"rememberMeParameterName")).isEqualTo("test-param");
}
@Test
public void createWithNullParameter() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("rememberMeParameterName cannot be empty or null");
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setRememberMeParameterName(null);
}
@Test
public void createWithAlwaysRemember() {
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setAlwaysRemember(true);
assertThat(
ReflectionTestUtils.getField(this.rememberMeServices, "alwaysRemember"))
.isEqualTo(true);
}
@Test
public void createWithCustomValidity() {
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setValiditySeconds(100000);
assertThat(
ReflectionTestUtils.getField(this.rememberMeServices, "validitySeconds"))
.isEqualTo(100000);
}
@Test
public void autoLogin() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.autoLogin(request, response);
verifyZeroInteractions(request, response);
}
// gh-752
@Test
public void loginFailRemoveSecurityContext() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
HttpSession session = mock(HttpSession.class);
given(request.getSession(eq(false))).willReturn(session);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.loginFail(request, response);
verify(request, times(1)).getSession(eq(false));
verify(session, times(1)).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
verifyZeroInteractions(request, response, session);
}
@Test
public void loginSuccess() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Authentication authentication = mock(Authentication.class);
HttpSession session = mock(HttpSession.class);
given(request.getParameter(eq("remember-me"))).willReturn("true");
given(request.getSession()).willReturn(session);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.loginSuccess(request, response, authentication);
verify(request, times(1)).getParameter(eq("remember-me"));
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(
eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
}
@Test
public void loginSuccessWithCustomParameter() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Authentication authentication = mock(Authentication.class);
HttpSession session = mock(HttpSession.class);
given(request.getParameter(eq("test-param"))).willReturn("true");
given(request.getSession()).willReturn(session);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setRememberMeParameterName("test-param");
this.rememberMeServices.loginSuccess(request, response, authentication);
verify(request, times(1)).getParameter(eq("test-param"));
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(
eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
}
@Test
public void loginSuccessWithAlwaysRemember() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Authentication authentication = mock(Authentication.class);
HttpSession session = mock(HttpSession.class);
given(request.getSession()).willReturn(session);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setAlwaysRemember(true);
this.rememberMeServices.loginSuccess(request, response, authentication);
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(
eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(2592000));
verifyZeroInteractions(request, response, session, authentication);
}
@Test
public void loginSuccessWithCustomValidity() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Authentication authentication = mock(Authentication.class);
HttpSession session = mock(HttpSession.class);
given(request.getParameter(eq("remember-me"))).willReturn("true");
given(request.getSession()).willReturn(session);
this.rememberMeServices = new SpringSessionRememberMeServices();
this.rememberMeServices.setValiditySeconds(100000);
this.rememberMeServices.loginSuccess(request, response, authentication);
verify(request, times(1)).getParameter(eq("remember-me"));
verify(request, times(1)).getSession();
verify(request, times(1)).setAttribute(
eq(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR), eq(true));
verify(session, times(1)).setMaxInactiveInterval(eq(100000));
verifyZeroInteractions(request, response, session, authentication);
}
}

View File

@@ -0,0 +1,746 @@
/*
* Copyright 2014-2017 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.web.http;
import java.util.Base64;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import static org.assertj.core.api.Assertions.assertThat;
public class CookieHttpSessionStrategyTests {
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private CookieHttpSessionStrategy strategy;
private String cookieName;
private Session session;
@Before
public void setup() throws Exception {
this.cookieName = "SESSION";
this.session = new MapSession();
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.strategy = new CookieHttpSessionStrategy();
}
@Test
public void getRequestedSessionIdNull() throws Exception {
assertThat(this.strategy.getRequestedSessionId(this.request)).isNull();
}
@Test
public void getRequestedSessionIdNotNull() throws Exception {
setSessionCookie(this.session.getId());
assertThat(this.strategy.getRequestedSessionId(this.request))
.isEqualTo(this.session.getId());
}
@Test
public void getRequestedSessionIdNotNullCustomCookieName() throws Exception {
setCookieName("CUSTOM");
setSessionCookie(this.session.getId());
assertThat(this.strategy.getRequestedSessionId(this.request))
.isEqualTo(this.session.getId());
}
@Test
public void onNewSession() throws Exception {
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo(this.session.getId());
}
@Test
public void onNewSessionTwiceSameId() throws Exception {
this.strategy.onNewSession(this.session, this.request, this.response);
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(this.response.getCookies()).hasSize(1);
}
@Test
public void onNewSessionTwiceNewId() throws Exception {
Session newSession = new MapSession();
this.strategy.onNewSession(this.session, this.request, this.response);
this.strategy.onNewSession(newSession, this.request, this.response);
Cookie[] cookies = this.response.getCookies();
assertThat(cookies).hasSize(2);
assertThat(base64Decode(cookies[0].getValue())).isEqualTo(this.session.getId());
assertThat(base64Decode(cookies[1].getValue())).isEqualTo(newSession.getId());
}
@Test
public void onNewSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionCookie(existing.getId());
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo(this.session.getId());
}
@Test
public void onNewSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionCookie(existing.getId());
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId())
.isEqualTo("0 " + existing.getId() + " new " + this.session.getId());
}
@Test
public void onNewSessionExistingSessionNewAliasCustomDelimiter() throws Exception {
this.strategy.setSerializationDelimiter("_");
Session existing = new MapSession();
setSessionCookie(existing.getId());
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId())
.isEqualTo("0_" + existing.getId() + "_new_" + this.session.getId());
}
// gh-321
@Test
public void onNewSessionExplicitAlias() throws Exception {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo("new " + this.session.getId());
}
@Test
public void onNewSessionCookiePath() throws Exception {
this.request.setContextPath("/somethingunique");
this.strategy.onNewSession(this.session, this.request, this.response);
Cookie sessionCookie = this.response.getCookie(this.cookieName);
assertThat(sessionCookie.getPath())
.isEqualTo(this.request.getContextPath() + "/");
}
@Test
public void onNewSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo(this.session.getId());
}
@Test
public void onDeleteSession() throws Exception {
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEmpty();
}
@Test
public void onDeleteSessionCookiePath() throws Exception {
this.request.setContextPath("/somethingunique");
this.strategy.onInvalidateSession(this.request, this.response);
Cookie sessionCookie = this.response.getCookie(this.cookieName);
assertThat(sessionCookie.getPath())
.isEqualTo(this.request.getContextPath() + "/");
}
@Test
public void onDeleteSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEmpty();
}
@Test
public void onDeleteSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionCookie("0 " + existing.getId() + " new " + this.session.getId());
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEqualTo(existing.getId());
}
@Test
public void onDeleteSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionCookie("0 " + existing.getId() + " new " + this.session.getId());
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEqualTo(existing.getId());
}
@Test
public void encodeURLNoExistingQuery() {
assertThat(this.strategy.encodeURL("/url", "2")).isEqualTo("/url?_s=2");
}
@Test
public void encodeURLNoExistingQueryEmpty() {
assertThat(this.strategy.encodeURL("/url?", "2")).isEqualTo("/url?_s=2");
}
@Test
public void encodeURLExistingQueryNoAlias() {
assertThat(this.strategy.encodeURL("/url?a=b", "2")).isEqualTo("/url?a=b&_s=2");
}
@Test
public void encodeURLExistingQueryExistingAliasStart() {
assertThat(this.strategy.encodeURL("/url?_s=1&y=z", "2"))
.isEqualTo("/url?_s=2&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasMiddle() {
assertThat(this.strategy.encodeURL("/url?a=b&_s=1&y=z", "2"))
.isEqualTo("/url?a=b&_s=2&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasEnd() {
assertThat(this.strategy.encodeURL("/url?a=b&_s=1", "2"))
.isEqualTo("/url?a=b&_s=2");
}
//
@Test
public void encodeURLExistingQueryParamEndsWithActualParamStart() {
assertThat(this.strategy.encodeURL("/url?x_s=1&y=z", "2"))
.isEqualTo("/url?x_s=1&y=z&_s=2");
}
@Test
public void encodeURLExistingQueryParamEndsWithActualParamMiddle() {
assertThat(this.strategy.encodeURL("/url?a=b&x_s=1&y=z", "2"))
.isEqualTo("/url?a=b&x_s=1&y=z&_s=2");
}
@Test
public void encodeURLExistingQueryParamEndsWithActualParamEnd() {
assertThat(this.strategy.encodeURL("/url?a=b&x_s=1", "2"))
.isEqualTo("/url?a=b&x_s=1&_s=2");
}
//
@Test
public void encodeURLNoExistingQueryDefaultAlias() {
assertThat(this.strategy.encodeURL("/url", "0")).isEqualTo("/url");
}
@Test
public void encodeURLNoExistingQueryEmptyDefaultAlias() {
assertThat(this.strategy.encodeURL("/url?", "0")).isEqualTo("/url?");
}
@Test
public void encodeURLExistingQueryNoAliasDefaultAlias() {
assertThat(this.strategy.encodeURL("/url?a=b", "0")).isEqualTo("/url?a=b");
}
@Test
public void encodeURLExistingQueryExistingAliasStartDefaultAlias() {
// relaxed constraint as result /url?&y=z does not hurt anything (ideally should
// remove the &)
assertThat(this.strategy.encodeURL("/url?_s=1&y=z", "0"))
.doesNotContain("_s=0&_s=1");
}
@Test
public void encodeURLExistingQueryExistingAliasMiddleDefaultAlias() {
assertThat(this.strategy.encodeURL("/url?a=b&_s=1&y=z", "0"))
.isEqualTo("/url?a=b&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasEndDefaultAlias() {
assertThat(this.strategy.encodeURL("/url?a=b&_s=1", "0")).isEqualTo("/url?a=b");
}
@Test
public void encodeURLWithSameAlias() {
String url = String.format("/url?%s=1",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
assertThat(this.strategy.encodeURL(url, "1")).isEqualTo(url);
}
@Test
public void encodeURLWithSameAliasOtherQueryParamsBefore() {
String url = String.format("/url?a=b&%s=1",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
assertThat(this.strategy.encodeURL(url, "1")).isEqualTo(url);
}
@Test
public void encodeURLWithSameAliasOtherQueryParamsAfter() {
String url = String.format("/url?%s=1&a=b",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
assertThat(this.strategy.encodeURL(url, "1")).isEqualTo(url);
}
@Test
public void encodeURLWithSameAliasOtherQueryParamsBeforeAndAfter() {
String url = String.format("/url?a=b&%s=1&c=d",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
assertThat(this.strategy.encodeURL(url, "1")).isEqualTo(url);
}
@Test
public void encodeURLMaliciousAlias() {
assertThat(this.strategy.encodeURL("/url?a=b&_s=1",
"\"> <script>alert('hi')</script>")).isEqualTo(
"/url?a=b&_s=%22%3E+%3Cscript%3Ealert%28%27hi%27%29%3C%2Fscript%3E");
}
// --- getCurrentSessionAlias
@Test
public void getCurrentSessionAliasNull() {
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasNullParamName() {
this.strategy.setSessionAliasParamName(null);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "NOT USED");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
// protect against malicious users
@Test
public void getCurrentSessionAliasContainsQuote() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "here\"this");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasContainsSingleQuote() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "here'this");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasContainsSpace() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "here this");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasContainsLt() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "here<this");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasContainsGt() {
this.strategy.setSessionAliasParamName(null);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "here>this");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasTooLong() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME,
"012345678901234567890123456789012345678901234567890");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
// We want some sort of length restrictions, but want to ensure some sort of length
// Technically no hard limit, but chose 50
@Test
public void getCurrentSessionAliasAllows50() {
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME,
"01234567890123456789012345678901234567890123456789");
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo("01234567890123456789012345678901234567890123456789");
}
@Test
public void getCurrentSession() {
String expectedAlias = "1";
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME,
expectedAlias);
assertThat(this.strategy.getCurrentSessionAlias(this.request))
.isEqualTo(expectedAlias);
}
// --- getNewSessionAlias
@Test
public void getNewSessionAliasNoSessions() {
assertThat(this.strategy.getNewSessionAlias(this.request))
.isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getNewSessionAliasSingleSession() {
setSessionCookie("abc");
assertThat(this.strategy.getNewSessionAlias(this.request)).isEqualTo("1");
}
@Test
public void getNewSessionAlias2Sessions() {
setCookieWithNSessions(2);
assertThat(this.strategy.getNewSessionAlias(this.request)).isEqualTo("2");
}
@Test
public void getNewSessionAlias9Sessions() {
setCookieWithNSessions(9);
assertThat(this.strategy.getNewSessionAlias(this.request))
.isEqualToIgnoringCase("9");
}
@Test
public void getNewSessionAlias10Sessions() {
setCookieWithNSessions(10);
assertThat(this.strategy.getNewSessionAlias(this.request))
.isEqualToIgnoringCase("a");
}
@Test
public void getNewSessionAlias16Sessions() {
setCookieWithNSessions(16);
assertThat(this.strategy.getNewSessionAlias(this.request))
.isEqualToIgnoringCase("10");
}
@Test
public void getNewSessionAliasInvalidAlias() {
setSessionCookie("0 1 $ b");
assertThat(this.strategy.getNewSessionAlias(this.request))
.isEqualToIgnoringCase("1");
}
// --- getSessionIds
@Test
public void getSessionIdsNone() {
assertThat(this.strategy.getSessionIds(this.request)).isEmpty();
}
@Test
public void getSessionIdsSingle() {
String expectedId = "a";
setSessionCookie(expectedId);
Map<String, String> sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(1);
assertThat(sessionIds.get("0")).isEqualTo(expectedId);
}
@Test
public void getSessionIdsMulti() {
setSessionCookie("0 a 1 b");
Map<String, String> sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
}
@Test
public void getSessionIdsMultiCustomDelimeter() {
this.strategy.setDeserializationDelimiter("_");
setSessionCookie("0_a_1_b");
Map<String, String> sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
}
@Test
public void getSessionIdsMultiCustomDelimeterMigration() {
this.strategy.setDeserializationDelimiter("_ ");
this.strategy.setSerializationDelimiter("_");
// can parse the old way
setSessionCookie("0 a 1 b");
Map<String, String> sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
// can parse the new way
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
setSessionCookie("0_a_1_b");
sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
// writes the new way
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
Session existing = new MapSession();
setSessionCookie(existing.getId());
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId())
.isEqualTo("0_" + existing.getId() + "_new_" + this.session.getId());
}
@Test
public void getSessionIdsDangling() {
setSessionCookie("0 a 1 b noValue");
Map<String, String> sessionIds = this.strategy.getSessionIds(this.request);
assertThat(sessionIds.size()).isEqualTo(2);
assertThat(sessionIds.get("0")).isEqualTo("a");
assertThat(sessionIds.get("1")).isEqualTo("b");
}
// --- helper
@Test
public void createSessionCookieValue() {
assertThat(createSessionCookieValue(17)).isEqualToIgnoringCase(
"0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 a 10 b 11 c 12 d 13 e 14 f 15 10 16");
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlDoesntContainAliasCurrentReqNoAlias() {
String url = "http://www.somehost.com/some/path";
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(url);
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlDoesntContainAliasCurrentReqHasAlias() {
String url = "http://www.somehost.com/some/path";
String alias = "1";
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias);
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(String.format("%s?%s=%s", url,
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias));
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlContainsAliasCurrentReqHasNoAlias() {
String url = String.format("http://www.somehost.com/some/path?%s=5",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "4");
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(url);
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlDoesntContainAliasCurrentReqNoAliasWithOtherParams() {
String url = "http://www.somehost.com/some/path?a=b";
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(url);
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlDoesntContainAliasCurrentReqHasAliasWithOtherParams() {
String url = "http://www.somehost.com/some/path?a=b";
String alias = "1";
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias);
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(String.format("%s&%s=%s", url,
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias));
}
@Test
public void responseEncodeRedirectUrlWhereRedirectUrlContainsAliasCurrentReqHasNoAliasWithOtherParams() {
String url = String.format("http://www.somehost.com/some/path?a=b&%s=5&c=d",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "4");
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedRedirectUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedRedirectUrl).isEqualTo(url);
}
@Test
public void responseEncodeUrlWhereRedirectUrlDoesntContainAliasCurrentReqNoAlias() {
String url = "http://www.somehost.com/some/path";
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(url);
}
@Test
public void responseEncodeUrlWhereRedirectUrlDoesntContainAliasCurrentReqHasAlias() {
String url = "http://www.somehost.com/some/path";
String alias = "1";
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias);
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(String.format("%s?%s=%s", url,
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias));
}
@Test
public void responseEncodeUrlWhereRedirectUrlContainsAliasCurrentReqHasNoAlias() {
String url = String.format("http://www.somehost.com/some/path?%s=5",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "4");
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(url);
}
@Test
public void responseEncodeUrlWhereRedirectUrlDoesntContainAliasCurrentReqNoAliasWithOtherParams() {
String url = "http://www.somehost.com/some/path?a=b";
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(url);
}
@Test
public void responseEncodeUrlWhereRedirectUrlDoesntContainAliasCurrentReqHasAliasWithOtherParams() {
String url = "http://www.somehost.com/some/path?a=b";
String alias = "1";
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias);
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(String.format("%s&%s=%s", url,
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, alias));
}
@Test
public void responseEncodeUrlWhereRedirectUrlContainsAliasCurrentReqHasNoAliasWithOtherParams() {
String url = String.format("http://www.somehost.com/some/path?a=b&%s=5&c=d",
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME);
this.request.setParameter(
CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "4");
HttpServletResponse wrappedResponse = this.strategy.wrapResponse(this.request,
this.response);
String encodedUrl = wrappedResponse.encodeRedirectURL(url);
assertThat(encodedUrl).isEqualTo(url);
}
private void setCookieWithNSessions(long size) {
setSessionCookie(createSessionCookieValue(size));
}
private String createSessionCookieValue(long size) {
StringBuilder sb = new StringBuilder();
for (long i = 0; i < size; i++) {
String hex = Long.toHexString(i);
sb.append(hex);
sb.append(" ");
sb.append(i);
if (i < size - 1) {
sb.append(" ");
}
}
return sb.toString();
}
public void setCookieName(String cookieName) {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookieName(cookieName);
this.strategy.setCookieSerializer(cookieSerializer);
this.cookieName = cookieName;
}
public void setSessionCookie(String value) {
this.request.setCookies(new Cookie(this.cookieName, base64Encode(value)));
}
public String getSessionId() {
return base64Decode(this.response.getCookie(this.cookieName).getValue());
}
private static String base64Encode(String value) {
return Base64.getEncoder().encodeToString(value.getBytes());
}
private static String base64Decode(String value) {
return new String(Base64.getDecoder().decode(value));
}
}

View File

@@ -0,0 +1,477 @@
/*
* 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.web.http;
import java.util.Base64;
import javax.servlet.http.Cookie;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultCookieSerializer}.
*
* @author Rob Winch
* @author Vedran Pavic
* @author Eddú Meléndez
*/
@RunWith(Parameterized.class)
public class DefaultCookieSerializerTests {
@Parameters(name = "useBase64Encoding={0}")
public static Object[] parameters() {
return new Object[] { false, true };
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private boolean useBase64Encoding;
private String cookieName;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private DefaultCookieSerializer serializer;
private String sessionId;
public DefaultCookieSerializerTests(boolean useBase64Encoding) {
this.useBase64Encoding = useBase64Encoding;
}
@Before
public void setup() {
this.cookieName = "SESSION";
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.sessionId = "sessionId";
this.serializer = new DefaultCookieSerializer();
this.serializer.setUseBase64Encoding(this.useBase64Encoding);
}
// --- readCookieValues ---
@Test
public void readCookieValuesNull() {
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@Test
public void readCookieValuesSingle() {
this.request.setCookies(createCookie(this.cookieName, this.sessionId));
assertThat(this.serializer.readCookieValues(this.request))
.containsOnly(this.sessionId);
}
@Test
public void readCookieSerializerUseBase64EncodingTrueValuesNotBase64() {
this.sessionId = "&^%$*";
this.serializer.setUseBase64Encoding(true);
this.request.setCookies(new Cookie(this.cookieName, this.sessionId));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@Test
public void readCookieValuesSingleAndInvalidName() {
this.request.setCookies(createCookie(this.cookieName, this.sessionId),
createCookie(this.cookieName + "INVALID", this.sessionId + "INVALID"));
assertThat(this.serializer.readCookieValues(this.request))
.containsOnly(this.sessionId);
}
@Test
public void readCookieValuesMulti() {
String secondSession = "secondSessionId";
this.request.setCookies(createCookie(this.cookieName, this.sessionId),
createCookie(this.cookieName, secondSession));
assertThat(this.serializer.readCookieValues(this.request))
.containsExactly(this.sessionId, secondSession);
}
@Test
public void readCookieValuesMultiCustomSessionCookieName() {
setCookieName("JSESSIONID");
String secondSession = "secondSessionId";
this.request.setCookies(createCookie(this.cookieName, this.sessionId),
createCookie(this.cookieName, secondSession));
assertThat(this.serializer.readCookieValues(this.request))
.containsExactly(this.sessionId, secondSession);
}
// gh-392
@Test
public void readCookieValuesNullCookieValue() {
this.request.setCookies(createCookie(this.cookieName, null));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@Test
public void readCookieValuesNullCookieValueAndJvmRoute() {
this.serializer.setJvmRoute("123");
this.request.setCookies(createCookie(this.cookieName, null));
assertThat(this.serializer.readCookieValues(this.request)).isEmpty();
}
@Test
public void readCookieValuesNullCookieValueAndNotNullCookie() {
this.serializer.setJvmRoute("123");
this.request.setCookies(createCookie(this.cookieName, null),
createCookie(this.cookieName, this.sessionId));
assertThat(this.serializer.readCookieValues(this.request))
.containsOnly(this.sessionId);
}
// --- writeCookie ---
@Test
public void writeCookie() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookieValue()).isEqualTo(this.sessionId);
}
// --- httpOnly ---
@Test
public void writeCookieHttpOnlyDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
public void writeCookieHttpOnlySetTrue() {
this.serializer.setUseHttpOnlyCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
public void writeCookieHttpOnlySetFalse() {
this.serializer.setUseHttpOnlyCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().isHttpOnly()).isFalse();
}
// --- domainName ---
@Test
public void writeCookieDomainNameDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isNull();
}
@Test
public void writeCookieDomainNameCustom() {
String domainName = "example.com";
this.serializer.setDomainName(domainName);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isEqualTo(domainName);
}
@Test(expected = IllegalStateException.class)
public void setDomainNameAndDomainNamePatternThrows() {
this.serializer.setDomainName("example.com");
this.serializer.setDomainNamePattern(".*");
}
// --- domainNamePattern ---
@Test
public void writeCookieDomainNamePattern() {
String domainNamePattern = "^.+?\\.(\\w+\\.[a-z]+)$";
this.serializer.setDomainNamePattern(domainNamePattern);
String[] matchingDomains = { "child.sub.example.com", "www.example.com" };
for (String domain : matchingDomains) {
this.request.setServerName(domain);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isEqualTo("example.com");
this.response = new MockHttpServletResponse();
}
String[] notMatchingDomains = { "example.com", "localhost", "127.0.0.1" };
for (String domain : notMatchingDomains) {
this.request.setServerName(domain);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getDomain()).isNull();
this.response = new MockHttpServletResponse();
}
}
@Test(expected = IllegalStateException.class)
public void setDomainNamePatternAndDomainNameThrows() {
this.serializer.setDomainNamePattern(".*");
this.serializer.setDomainName("example.com");
}
// --- cookieName ---
@Test
public void writeCookieCookieNameDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getName()).isEqualTo("SESSION");
}
@Test
public void writeCookieCookieNameCustom() {
String cookieName = "JSESSIONID";
setCookieName(cookieName);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getName()).isEqualTo(cookieName);
}
@Test(expected = IllegalArgumentException.class)
public void setCookieNameNullThrows() {
this.serializer.setCookieName(null);
}
// --- cookiePath ---
@Test
public void writeCookieCookiePathDefaultEmptyContextPathUsed() {
this.request.setContextPath("");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
@Test
public void writeCookieCookiePathDefaultContextPathUsed() {
this.request.setContextPath("/context");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@Test
public void writeCookieCookiePathExplicitNullCookiePathContextPathUsed() {
this.request.setContextPath("/context");
this.serializer.setCookiePath(null);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@Test
public void writeCookieCookiePathExplicitCookiePath() {
this.request.setContextPath("/context");
this.serializer.setCookiePath("/");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
// --- cookieMaxAge ---
@Test
public void writeCookieCookieMaxAgeDefault() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(-1);
}
@Test
public void writeCookieCookieMaxAgeExplicit() {
this.serializer.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(100);
}
@Test
public void writeCookieCookieMaxAgeExplicitEmptyCookie() {
this.serializer.setCookieMaxAge(100);
this.serializer.writeCookieValue(cookieValue(""));
assertThat(getCookie().getMaxAge()).isEqualTo(0);
}
// --- secure ---
@Test
public void writeCookieDefaultInsecureRequest() {
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@Test
public void writeCookieSecureSecureRequest() {
this.request.setSecure(true);
this.serializer.setUseSecureCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@Test
public void writeCookieSecureInsecureRequest() {
this.serializer.setUseSecureCookie(true);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@Test
public void writeCookieInsecureSecureRequest() {
this.request.setSecure(true);
this.serializer.setUseSecureCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@Test
public void writeCookieInecureInsecureRequest() {
this.serializer.setUseSecureCookie(false);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
// --- jvmRoute ---
@Test
public void writeCookieJvmRoute() {
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookieValue()).isEqualTo(this.sessionId + "." + jvmRoute);
}
@Test
public void readCookieJvmRoute() {
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(
createCookie(this.cookieName, this.sessionId + "." + jvmRoute));
assertThat(this.serializer.readCookieValues(this.request))
.containsOnly(this.sessionId);
}
@Test
public void readCookieJvmRouteRouteMissing() {
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(createCookie(this.cookieName, this.sessionId));
assertThat(this.serializer.readCookieValues(this.request))
.containsOnly(this.sessionId);
}
@Test
public void readCookieJvmRouteOnlyRoute() {
String jvmRoute = "route";
this.serializer.setJvmRoute(jvmRoute);
this.request.setCookies(createCookie(this.cookieName, "." + jvmRoute));
assertThat(this.serializer.readCookieValues(this.request)).containsOnly("");
}
// --- rememberMe ---
@Test
public void writeCookieRememberMeCookieMaxAgeDefault() {
this.request.setAttribute("rememberMe", true);
this.serializer.setRememberMeRequestAttribute("rememberMe");
this.serializer.writeCookieValue(cookieValue(this.sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(Integer.MAX_VALUE);
}
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
this.serializer.setCookieName(cookieName);
}
private Cookie createCookie(String name, String value) {
if (this.useBase64Encoding && StringUtils.hasLength(value)) {
value = new String(Base64.getEncoder().encode(value.getBytes()));
}
return new Cookie(name, value);
}
private Cookie getCookie() {
return this.response.getCookie(this.cookieName);
}
private String getCookieValue() {
String value = getCookie().getValue();
if (!this.useBase64Encoding) {
return value;
}
if (value == null) {
return null;
}
return new String(Base64.getDecoder().decode(value));
}
private CookieValue cookieValue(String cookieValue) {
return new CookieValue(this.request, this.response, cookieValue);
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2014-2017 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.web.http;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import static org.assertj.core.api.Assertions.assertThat;
public class HeaderSessionStrategyTests {
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private HeaderHttpSessionStrategy strategy;
private String headerName;
private Session session;
@Before
public void setup() throws Exception {
this.headerName = "X-Auth-Token";
this.session = new MapSession();
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.strategy = new HeaderHttpSessionStrategy();
}
@Test
public void getRequestedSessionIdNull() throws Exception {
assertThat(this.strategy.getRequestedSessionId(this.request)).isNull();
}
@Test
public void getRequestedSessionIdNotNull() throws Exception {
setSessionId(this.session.getId());
assertThat(this.strategy.getRequestedSessionId(this.request))
.isEqualTo(this.session.getId());
}
@Test
public void getRequestedSessionIdNotNullCustomHeaderName() throws Exception {
setHeaderName("CUSTOM");
setSessionId(this.session.getId());
assertThat(this.strategy.getRequestedSessionId(this.request))
.isEqualTo(this.session.getId());
}
@Test
public void onNewSession() throws Exception {
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo(this.session.getId());
}
// the header is set as apposed to added
@Test
public void onNewSessionMulti() throws Exception {
this.strategy.onNewSession(this.session, this.request, this.response);
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(this.response.getHeaders(this.headerName).size()).isEqualTo(1);
assertThat(this.response.getHeaders(this.headerName))
.containsOnly(this.session.getId());
}
@Test
public void onNewSessionCustomHeaderName() throws Exception {
setHeaderName("CUSTOM");
this.strategy.onNewSession(this.session, this.request, this.response);
assertThat(getSessionId()).isEqualTo(this.session.getId());
}
@Test
public void onDeleteSession() throws Exception {
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEmpty();
}
// the header is set as apposed to added
@Test
public void onDeleteSessionMulti() throws Exception {
this.strategy.onInvalidateSession(this.request, this.response);
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(this.response.getHeaders(this.headerName).size()).isEqualTo(1);
assertThat(getSessionId()).isEmpty();
}
@Test
public void onDeleteSessionCustomHeaderName() throws Exception {
setHeaderName("CUSTOM");
this.strategy.onInvalidateSession(this.request, this.response);
assertThat(getSessionId()).isEmpty();
}
@Test(expected = IllegalArgumentException.class)
public void setHeaderNameNull() throws Exception {
this.strategy.setHeaderName(null);
}
public void setHeaderName(String headerName) {
this.strategy.setHeaderName(headerName);
this.headerName = headerName;
}
public void setSessionId(String id) {
this.request.addHeader(this.headerName, id);
}
public String getSessionId() {
return this.response.getHeader(this.headerName);
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2014-2017 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.web.http;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
public class OncePerRequestFilterTests {
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private MockFilterChain chain;
private OncePerRequestFilter filter;
private HttpServlet servlet;
private List<OncePerRequestFilter> invocations;
@Before
@SuppressWarnings("serial")
public void setup() {
this.servlet = new HttpServlet() {
};
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.chain = new MockFilterChain();
this.invocations = new ArrayList<>();
this.filter = new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
OncePerRequestFilterTests.this.invocations.add(this);
filterChain.doFilter(request, response);
}
};
}
@Test
public void doFilterOnce() throws ServletException, IOException {
this.filter.doFilter(this.request, this.response, this.chain);
assertThat(this.invocations).containsOnly(this.filter);
}
@Test
public void doFilterMultiOnlyIvokesOnce() throws ServletException, IOException {
this.filter.doFilter(this.request, this.response,
new MockFilterChain(this.servlet, this.filter));
assertThat(this.invocations).containsOnly(this.filter);
}
@Test
public void doFilterOtherSubclassInvoked() throws ServletException, IOException {
OncePerRequestFilter filter2 = new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
OncePerRequestFilterTests.this.invocations.add(this);
filterChain.doFilter(request, response);
}
};
this.filter.doFilter(this.request, this.response,
new MockFilterChain(this.servlet, filter2));
assertThat(this.invocations).containsOnly(this.filter, filter2);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2014-2017 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.web.http;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* @author Rob Winch
* @since 1.1
*/
@RunWith(MockitoJUnitRunner.class)
public class SessionEventHttpSessionListenerAdapterTests {
@Mock
HttpSessionListener listener1;
@Mock
HttpSessionListener listener2;
@Mock
ServletContext servletContext;
@Captor
ArgumentCaptor<HttpSessionEvent> sessionEvent;
SessionDestroyedEvent destroyed;
SessionCreatedEvent created;
SessionEventHttpSessionListenerAdapter listener;
@Before
public void setup() {
this.listener = new SessionEventHttpSessionListenerAdapter(
Arrays.asList(this.listener1, this.listener2));
Session session = new MapSession();
this.destroyed = new SessionDestroyedEvent(this, session);
this.created = new SessionCreatedEvent(this, session);
}
// We want relaxed constructor that will allow for an empty listeners to
// make configuration easier (i.e. autowire all HttpSessionListeners and might get
// none)
@Test
public void constructorEmptyWorks() {
new SessionEventHttpSessionListenerAdapter(
Collections.<HttpSessionListener>emptyList());
}
/**
* Make sure that we short circuit onApplicationEvent as early as possible if no
* listeners
*/
@Test
public void onApplicationEventEmptyListenersDoesNotUseEvent() {
this.listener = new SessionEventHttpSessionListenerAdapter(
Collections.<HttpSessionListener>emptyList());
this.destroyed = mock(SessionDestroyedEvent.class);
this.listener.onApplicationEvent(this.destroyed);
verifyZeroInteractions(this.destroyed, this.listener1, this.listener2);
}
@Test
public void onApplicationEventDestroyed() {
this.listener.onApplicationEvent(this.destroyed);
verify(this.listener1).sessionDestroyed(this.sessionEvent.capture());
verify(this.listener2).sessionDestroyed(this.sessionEvent.capture());
assertThat(this.sessionEvent.getValue().getSession().getId())
.isEqualTo(this.destroyed.getSessionId());
}
@Test
public void onApplicationEventCreated() {
this.listener.onApplicationEvent(this.created);
verify(this.listener1).sessionCreated(this.sessionEvent.capture());
verify(this.listener2).sessionCreated(this.sessionEvent.capture());
assertThat(this.sessionEvent.getValue().getSession().getId())
.isEqualTo(this.created.getSessionId());
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2014-2017 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.web.socket.handler;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.session.web.socket.events.SessionConnectEvent;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class WebSocketConnectHandlerDecoratorFactoryTests {
@Mock
ApplicationEventPublisher eventPublisher;
@Mock
WebSocketHandler delegate;
@Mock
WebSocketSession session;
@Captor
ArgumentCaptor<SessionConnectEvent> event;
WebSocketConnectHandlerDecoratorFactory factory;
@Before
public void setup() {
this.factory = new WebSocketConnectHandlerDecoratorFactory(this.eventPublisher);
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullEventPublisher() {
new WebSocketConnectHandlerDecoratorFactory(null);
}
@Test
public void decorateAfterConnectionEstablished() throws Exception {
WebSocketHandler decorated = this.factory.decorate(this.delegate);
decorated.afterConnectionEstablished(this.session);
verify(this.eventPublisher).publishEvent(this.event.capture());
assertThat(this.event.getValue().getWebSocketSession()).isSameAs(this.session);
}
@Test
public void decorateAfterConnectionEstablishedEventError() throws Exception {
WebSocketHandler decorated = this.factory.decorate(this.delegate);
willThrow(new IllegalStateException("Test throw on publishEvent"))
.given(this.eventPublisher).publishEvent(any(ApplicationEvent.class));
decorated.afterConnectionEstablished(this.session);
verify(this.eventPublisher).publishEvent(any(SessionConnectEvent.class));
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2014-2017 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.web.socket.handler;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.socket.events.SessionConnectEvent;
import org.springframework.session.web.socket.server.SessionRepositoryMessageInterceptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class WebSocketRegistryListenerTests {
@Mock
WebSocketSession wsSession;
@Mock
WebSocketSession wsSession2;
@Mock
Message<byte[]> message;
@Mock
Principal principal;
SessionConnectEvent connect;
SessionConnectEvent connect2;
SessionDisconnectEvent disconnect;
SessionDeletedEvent deleted;
SessionExpiredEvent expired;
Map<String, Object> attributes;
String sessionId;
WebSocketRegistryListener listener;
@Before
public void setup() {
this.sessionId = "session-id";
this.attributes = new HashMap<>();
SessionRepositoryMessageInterceptor.setSessionId(this.attributes, this.sessionId);
given(this.wsSession.getAttributes()).willReturn(this.attributes);
given(this.wsSession.getPrincipal()).willReturn(this.principal);
given(this.wsSession.getId()).willReturn("wsSession-id");
given(this.wsSession2.getAttributes()).willReturn(this.attributes);
given(this.wsSession2.getPrincipal()).willReturn(this.principal);
given(this.wsSession2.getId()).willReturn("wsSession-id2");
Map<String, Object> headers = new HashMap<>();
headers.put(SimpMessageHeaderAccessor.SESSION_ATTRIBUTES, this.attributes);
given(this.message.getHeaders()).willReturn(new MessageHeaders(headers));
this.listener = new WebSocketRegistryListener();
this.connect = new SessionConnectEvent(this.listener, this.wsSession);
this.connect2 = new SessionConnectEvent(this.listener, this.wsSession2);
this.disconnect = new SessionDisconnectEvent(this.listener, this.message,
this.wsSession.getId(), CloseStatus.NORMAL);
this.deleted = new SessionDeletedEvent(this.listener, this.sessionId);
this.expired = new SessionExpiredEvent(this.listener, this.sessionId);
}
@Test
public void onApplicationEventConnectSessionDeleted() throws Exception {
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.deleted);
verify(this.wsSession).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
}
@Test
public void onApplicationEventConnectSessionExpired() throws Exception {
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.expired);
verify(this.wsSession).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
}
@Test
public void onApplicationEventConnectSessionDeletedNullPrincipal() throws Exception {
given(this.wsSession.getPrincipal()).willReturn(null);
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.deleted);
verify(this.wsSession, times(0)).close(any(CloseStatus.class));
}
@Test
public void onApplicationEventConnectDisconnect() throws Exception {
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.disconnect);
this.listener.onApplicationEvent(this.deleted);
verify(this.wsSession, times(0)).close(any(CloseStatus.class));
}
// gh-76
@Test
@SuppressWarnings("unchecked")
public void onApplicationEventConnectDisconnectCleanup() {
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.disconnect);
Map<String, Map<String, WebSocketSession>> httpSessionIdToWsSessions = (Map<String, Map<String, WebSocketSession>>) ReflectionTestUtils
.getField(this.listener, "httpSessionIdToWsSessions");
assertThat(httpSessionIdToWsSessions).isEmpty();
}
@Test
public void onApplicationEventConnectDisconnectNullSession() throws Exception {
this.listener.onApplicationEvent(this.connect);
this.attributes.clear();
this.listener.onApplicationEvent(this.disconnect);
// no exception
}
@Test
public void onApplicationEventConnectConnectDisonnect() throws Exception {
this.listener.onApplicationEvent(this.connect);
this.listener.onApplicationEvent(this.connect2);
this.listener.onApplicationEvent(this.disconnect);
this.listener.onApplicationEvent(this.deleted);
verify(this.wsSession2).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
verify(this.wsSession, times(0)).close(any(CloseStatus.class));
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2014-2017 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.web.socket.server;
import java.time.Instant;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpSession;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(MockitoJUnitRunner.class)
public class SessionRepositoryMessageInterceptorTests {
@Mock
SessionRepository<Session> sessionRepository;
@Mock
MessageChannel channel;
@Mock
Session session;
Message<?> createMessage;
SimpMessageHeaderAccessor headers;
SessionRepositoryMessageInterceptor<Session> interceptor;
@Before
public void setup() {
this.interceptor = new SessionRepositoryMessageInterceptor<>(
this.sessionRepository);
this.headers = SimpMessageHeaderAccessor.create();
this.headers.setSessionId("session");
this.headers.setSessionAttributes(new HashMap<>());
setMessageType(SimpMessageType.MESSAGE);
String sessionId = "http-session";
setSessionId(sessionId);
given(this.sessionRepository.getSession(sessionId)).willReturn(this.session);
}
@Test(expected = IllegalArgumentException.class)
public void preSendconstructorNullRepository() {
new SessionRepositoryMessageInterceptor<>(null);
}
@Test
public void preSendNullMessage() {
assertThat(this.interceptor.preSend(null, this.channel)).isNull();
}
@Test
public void preSendConnectAckDoesNotInvokeSessionRepository() {
setMessageType(SimpMessageType.CONNECT_ACK);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void preSendHeartbeatDoesNotInvokeSessionRepository() {
setMessageType(SimpMessageType.HEARTBEAT);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void preSendDisconnectDoesNotInvokeSessionRepository() {
setMessageType(SimpMessageType.DISCONNECT);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void preSendOtherDoesNotInvokeSessionRepository() {
setMessageType(SimpMessageType.OTHER);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test(expected = IllegalArgumentException.class)
public void setMatchingMessageTypesNull() {
this.interceptor.setMatchingMessageTypes(null);
}
@Test(expected = IllegalArgumentException.class)
public void setMatchingMessageTypesEmpty() {
this.interceptor.setMatchingMessageTypes(Collections.<SimpMessageType>emptySet());
}
@Test
public void preSendSetMatchingMessageTypes() {
this.interceptor.setMatchingMessageTypes(EnumSet.of(SimpMessageType.DISCONNECT));
setMessageType(SimpMessageType.DISCONNECT);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verify(this.sessionRepository).getSession(anyString());
verify(this.sessionRepository).save(this.session);
}
@Test
public void preSendConnectUpdatesLastUpdateTime() {
setMessageType(SimpMessageType.CONNECT);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verify(this.session).setLastAccessedTime(argThat(isAlmostNow()));
verify(this.sessionRepository).save(this.session);
}
@Test
public void preSendMessageUpdatesLastUpdateTime() {
setMessageType(SimpMessageType.MESSAGE);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verify(this.session).setLastAccessedTime(argThat(isAlmostNow()));
verify(this.sessionRepository).save(this.session);
}
@Test
public void preSendSubscribeUpdatesLastUpdateTime() {
setMessageType(SimpMessageType.SUBSCRIBE);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verify(this.session).setLastAccessedTime(argThat(isAlmostNow()));
verify(this.sessionRepository).save(this.session);
}
@Test
public void preSendUnsubscribeUpdatesLastUpdateTime() {
setMessageType(SimpMessageType.UNSUBSCRIBE);
this.session.setLastAccessedTime(Instant.EPOCH);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verify(this.session).setLastAccessedTime(argThat(isAlmostNow()));
verify(this.sessionRepository).save(this.session);
}
// This will updated when SPR-12288 is resolved
@Test
public void preSendExpiredSession() {
setSessionId("expired");
this.interceptor.preSend(createMessage(), this.channel);
verify(this.sessionRepository, times(0)).save(any(Session.class));
}
@Test
public void preSendNullSessionId() {
setSessionId(null);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void preSendNullSessionAttributes() {
this.headers.setSessionAttributes(null);
assertThat(this.interceptor.preSend(createMessage(), this.channel))
.isSameAs(this.createMessage);
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void beforeHandshakeNotServletServerHttpRequest() throws Exception {
assertThat(this.interceptor.beforeHandshake(null, null, null, null)).isTrue();
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void beforeHandshakeNullSession() throws Exception {
ServletServerHttpRequest request = new ServletServerHttpRequest(
new MockHttpServletRequest());
assertThat(this.interceptor.beforeHandshake(request, null, null, null)).isTrue();
verifyZeroInteractions(this.sessionRepository);
}
@Test
public void beforeHandshakeSession() throws Exception {
MockHttpServletRequest httpRequest = new MockHttpServletRequest();
HttpSession httpSession = httpRequest.getSession();
ServletServerHttpRequest request = new ServletServerHttpRequest(httpRequest);
Map<String, Object> attributes = new HashMap<>();
assertThat(this.interceptor.beforeHandshake(request, null, null, attributes))
.isTrue();
assertThat(attributes.size()).isEqualTo(1);
assertThat(SessionRepositoryMessageInterceptor.getSessionId(attributes))
.isEqualTo(httpSession.getId());
}
/**
* At the moment there is no need for afterHandshake to do anything.
*/
@Test
public void afterHandshakeDoesNothing() {
this.interceptor.afterHandshake(null, null, null, null);
verifyZeroInteractions(this.sessionRepository);
}
private void setSessionId(String id) {
SessionRepositoryMessageInterceptor
.setSessionId(this.headers.getSessionAttributes(), id);
}
private Message<?> createMessage() {
this.createMessage = MessageBuilder.createMessage("",
this.headers.getMessageHeaders());
return this.createMessage;
}
private void setMessageType(SimpMessageType type) {
this.headers.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, type);
}
static AlmostNowMatcher isAlmostNow() {
return new AlmostNowMatcher();
}
static class AlmostNowMatcher implements ArgumentMatcher<Instant> {
public boolean matches(Instant argument) {
long now = System.currentTimeMillis();
long delta = now - argument.toEpochMilli();
return delta >= 0 && delta < TimeUnit.SECONDS.toMillis(3);
}
}
}