FindByPrincipalNameSessionRepository -> FindByIndexNameSessionRepository

Fixes gh-342
This commit is contained in:
Rob Winch
2016-01-29 13:38:33 -06:00
parent 7618aafb90
commit ad09b498a3
18 changed files with 267 additions and 180 deletions

View File

@@ -33,62 +33,28 @@ Consider the following scenario:
Wouldn't it be nice if we could allow the user to invalidate the session at the library from any device they authenticate with?
This sample demonstrates how this is possible.
[[findbyusernamesessionrepository]]
== FindByUsernameSessionRepository
[[findbyprincipalnamesessionrepository]]
== FindByIndexNameSessionRepository
In order to look up a user by their username, you must first choose a `SessionRepository` that implements <<index.doc#ap-findbyusernamesessionrepository,FindByUsernameSessionRepository>>.
In order to look up a user by their username, you must first choose a `SessionRepository` that implements <<index.doc#api-findbyprincipalnamesessionrepository,FindByIndexNameSessionRepository>>.
Our sample application assumes that the Redis support is already setup, so we are ready to go.
== Mapping the username
`FindByUsernameSessionRepository` can only find a session by the username, if the developer instructs Spring Session what user is associated with the `Session`.
This is done by ensuring that the session attribute with the name `Session.FindByUsernameSessionRepository` is populated with the username.
`FindByIndexNameSessionRepository` can only find a session by the username, if the developer instructs Spring Session what user is associated with the `Session`.
This is done by ensuring that the session attribute with the name `FindByUsernameSessionRepository.PRINCIPAL_NAME_INDEX_NAME` is populated with the username.
Generally, speaking this can be done with the following code immediately after the user authenticates:
[source,java,indent=0]
----
include::{samples-dir}findbyusername/src/main/java/sample/session/SpringSessionPrincipalNameSuccessHandler.java[tags=set-username]
include::{docs-test-dir}docs/FindByIndexNameSessionRepositoryTests.java[tags=set-username]
----
== Mapping the username with Spring Security
Since we are using Spring Security, it makes perfect sense to provide a custom `AuthenticationSuccessHandler` that populates the username.
For example:
[NOTE]
====
We plan to provide first class integration with Spring Security to make this process easier in the future.
For details track https://github.com/spring-projects/spring-session/issues/266[gh-266].
====
[source,java,indent=0]
----
include::{samples-dir}findbyusername/src/main/java/sample/session/SpringSessionPrincipalNameSuccessHandler.java[tags=class]
----
In order to support multiple handlers, we need to also create a custom `AuthenticationSuccessHandler` that delegates to multiple `AuthenticationSuccessHandler` instances.
For example:
[source,java,indent=0]
----
include::{samples-dir}findbyusername/src/main/java/sample/session/CompositeAuthenticationSuccessHandler.java[tags=class]
----
Next we need to provide a custom `AuthenticationSuccessHandler` with Spring Security.
In Java configuration, we can do this using the following:
[source,java,indent=0]
----
include::{samples-dir}findbyusername/src/main/java/sample/config/SecurityConfig.java[tags=handler]
----
We can then configure authentication success with the following:
[source,java,indent=0]
----
include::{samples-dir}findbyusername/src/main/java/sample/config/SecurityConfig.java[tags=config]
----
Since we are using Spring Security, the user name is automatically indexed for us.
This means we will not have to perform any steps to ensure the user name is indexed.
== Adding Additional Data to Session

View File

@@ -451,18 +451,37 @@ A `SessionRepository` is in charge of creating, retrieving, and persisting `Sess
If possible, developers should not interact directly with a `SessionRepository` or a `Session`.
Instead, developers should prefer interacting with `SessionRepository` and `Session` indirectly through the <<httpsession,HttpSession>> and <<websocket,WebSocket>> integration.
[[api-findbyusernamesessionrepository]]
=== FindByUsernameSessionRepository
[[api-findbyindexnamesessionrepository]]
=== FindByIndexNameSessionRepository
Spring Session's most basic API for using a `Session` is the `SessionRepository`.
This API is intentionally very simple, so that it is easy to provide additional implementations with basic functionality.
The `FindByUsernameSessionRepository` adds a single method to look up all the sessions for a particular user.
This is done by ensuring that the session attribute with the name `Session.PRINCIPAL_NAME_ATTRIBUTE_NAME` is populated with the username.
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.
Some `SessionRepository` implementations may choose to implement `FindByIndexNameSessionRepository` also.
For example, Spring's Redis support implements `FindByIndexNameSessionRepository`.
Some `SessionRepository` implementations may choose to implement `FindByUsernameSessionRepository` also.
For example, Spring's Redis support implements `FindByUsernameSessionRepository`.
The `FindByIndexNameSessionRepository` adds a single method to look up all the sessions for a particular user.
This is done by ensuring that the session attribute with the name `FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME` is populated with the username.
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.
An example of how this might be used can be seen below:
[source,java,indent=0]
----
include::{docs-test-dir}docs/FindByIndexNameSessionRepositoryTests.java[tags=set-username]
----
[NOTE]
====
Some implementations of `FindByIndexNameSessionRepository` will provide hooks to automatically index other session attributes.
For example, many implementations will automatically ensure the current Spring Security user name is indexed with the index name `FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`.
====
Once the session is indexed, it can be found using the following:
[source,java,indent=0]
----
include::{docs-test-dir}docs/FindByIndexNameSessionRepositoryTests.java[tags=findby-username]
----
[[api-enablespringhttpsession]]
=== EnableSpringHttpSession

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docs;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* @author Rob Winch
*
*/
@RunWith(MockitoJUnitRunner.class)
public class FindByIndexNameSessionRepositoryTests {
@Mock
FindByIndexNameSessionRepository<Session> sessionRepository;
@Mock
Session session;
@Test
public void setUsername() {
// tag::set-username[]
String username = "username";
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::set-username[]
}
@Test
public void findByUsername() {
// tag::findby-username[]
String username = "username";
Map<String,Session> sessionIdToSession =
sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::findby-username[]
}
}

View File

@@ -21,7 +21,7 @@ import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
@@ -38,12 +38,13 @@ import org.springframework.web.bind.annotation.RequestMethod;
public class IndexController {
// tag::findbyusername[]
@Autowired
FindByPrincipalNameSessionRepository<? extends ExpiringSession> sessions;
FindByIndexNameSessionRepository<? extends ExpiringSession> sessions;
@RequestMapping("/")
public String index(Principal principal, Model model) {
Collection<? extends ExpiringSession> usersSessions =
sessions.findByPrincipalName(principal.getName()).values();
sessions.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal.getName()).values();
model.addAttribute("sessions", usersSessions);
return "index";
}
@@ -51,7 +52,8 @@ public class IndexController {
@RequestMapping(value = "/sessions/{sessionIdToDelete}", method = RequestMethod.DELETE)
public String removeSession(Principal principal, @PathVariable String sessionIdToDelete) {
Set<String> usersSessionIds = sessions.findByPrincipalName(principal.getName()).keySet();
Set<String> usersSessionIds = sessions.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
principal.getName()).keySet();
if(usersSessionIds.contains(sessionIdToDelete)) {
sessions.delete(sessionIdToDelete);
}

View File

@@ -24,8 +24,7 @@ import javax.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.FindByIndexNameSessionRepository;
/**
* Inserts the username into Spring session after we successfully authenticate.
@@ -45,7 +44,7 @@ public class SpringSessionPrincipalNameSuccessHandler
String currentUsername = authentication.getName();
// tag::set-username[]
session.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, currentUsername);
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, currentUsername);
// end::set-username[]
}
}

View File

@@ -31,7 +31,7 @@ import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.data.gemfire.CacheFactoryBean;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -87,7 +87,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract
}
protected Map<String, ExpiringSession> doFindByPrincipalName(String principalName) {
return gemfireSessionRepository.findByPrincipalName(principalName);
return gemfireSessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
}
@SuppressWarnings("unchecked")
@@ -171,7 +171,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract
public void doesNotFindAfterPrincipalRemoved() {
String username = "doesNotFindAfterPrincipalRemoved";
ExpiringSession session = save(touch(createSession(username)));
session.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, null);
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, null);
save(session);
Map<String, ExpiringSession> nonExistingPrincipalSessions = doFindByPrincipalName(username);

View File

@@ -34,6 +34,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
@@ -66,7 +67,7 @@ public class RedisOperationsSessionRepositoryITests {
public void saves() throws InterruptedException {
String username = "saves-"+System.currentTimeMillis();
String usernameSessionKey = "spring:session:RedisOperationsSessionRepositoryITests:index:" + Session.PRINCIPAL_NAME_ATTRIBUTE_NAME + ":" + username;
String usernameSessionKey = "spring:session:RedisOperationsSessionRepositoryITests:index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + username;
RedisSession toSave = repository.createSession();
String expectedAttributeName = "a";
@@ -76,7 +77,7 @@ public class RedisOperationsSessionRepositoryITests {
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
toSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, username);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
registry.clear();
repository.save(toSave);
@@ -128,11 +129,11 @@ public class RedisOperationsSessionRepositoryITests {
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -140,7 +141,7 @@ public class RedisOperationsSessionRepositoryITests {
repository.delete(toSave.getId());
registry.receivedEvent();
findByPrincipalName = repository.findByPrincipalName(principalName);
findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
@@ -150,7 +151,7 @@ public class RedisOperationsSessionRepositoryITests {
public void findByPrincipalNameExpireRemovesIndex() throws Exception {
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
@@ -160,7 +161,7 @@ public class RedisOperationsSessionRepositoryITests {
byte[] pattern = new byte[] {};
repository.onMessage(message , pattern);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
@@ -170,14 +171,14 @@ public class RedisOperationsSessionRepositoryITests {
public void findByPrincipalNameNoPrincipalNameChange() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -187,7 +188,7 @@ public class RedisOperationsSessionRepositoryITests {
public void findByPrincipalNameNoPrincipalNameChangeReload() throws Exception {
String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
@@ -196,7 +197,7 @@ public class RedisOperationsSessionRepositoryITests {
toSave.setAttribute("other", "value");
repository.save(toSave);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -206,14 +207,14 @@ public class RedisOperationsSessionRepositoryITests {
public void findByDeletedPrincipalName() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, null);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, null);
repository.save(toSave);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).isEmpty();
}
@@ -223,17 +224,17 @@ public class RedisOperationsSessionRepositoryITests {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalNameChanged);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalNameChanged);
repository.save(toSave);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByPrincipalName(principalNameChanged);
findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -243,15 +244,15 @@ public class RedisOperationsSessionRepositoryITests {
public void findByDeletedPrincipalNameReload() throws Exception {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
RedisSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, null);
getSession.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, null);
repository.save(getSession);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).isEmpty();
}
@@ -261,19 +262,19 @@ public class RedisOperationsSessionRepositoryITests {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
RedisSession toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
toSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
repository.save(toSave);
RedisSession getSession = repository.getSession(toSave.getId());
getSession.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalNameChanged);
getSession.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalNameChanged);
repository.save(getSession);
Map<String, RedisSession> findByPrincipalName = repository.findByPrincipalName(principalName);
Map<String, RedisSession> findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = repository.findByPrincipalName(principalNameChanged);
findByPrincipalName = repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,principalNameChanged);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());

View File

@@ -29,6 +29,7 @@ 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.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.SessionEventRegistry;
@@ -88,7 +89,7 @@ public class EnableHazelcastHttpSessionEventsTests<S extends ExpiringSession> {
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, username);
sessionToSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
repository.save(sessionToSave);

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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}.
*
* @author Rob Winch
*
* @param <S>
* the type of Session being managed by this
* {@link FindByIndexNameSessionRepository}
*/
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

@@ -1,48 +0,0 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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 Session#PRINCIPAL_NAME_ATTRIBUTE_NAME}.
*
* @author Rob Winch
*
* @param <S>
* the type of Session being managed by this
* {@link FindByPrincipalNameSessionRepository}
*/
public interface FindByPrincipalNameSessionRepository<S extends Session> extends SessionRepository<S> {
/**
* Find a Map of the session id to the {@link Session} of all sessions that
* contain the session attribute with the name
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of the
* specified principal name.
*
* @param principalName
* the principal name (i.e. username) to search for
* @return a Map (never null) of the session id to the {@link Session} of
* all sessions that contain the session attribute with the name
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of
* the specified principal name. If no results are found, an empty
* Map is returned.
*/
Map<String, S> findByPrincipalName(String principalName);
}

View File

@@ -26,22 +26,6 @@ import java.util.Set;
*/
public interface Session {
/**
* <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_ATTRIBUTE_NAME = Session.class.getName().concat(".PRINCIPAL_NAME_ATTRIBUTE_NAME");
/**
* Gets a unique string that identifies the {@link Session}
*

View File

@@ -39,7 +39,7 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.gemfire.GemfireAccessor;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration;
import org.springframework.session.events.SessionCreatedEvent;
@@ -69,7 +69,7 @@ import com.gemstone.gemfire.cache.util.CacheListenerAdapter;
* @see org.springframework.data.gemfire.GemfireAccessor
* @see org.springframework.data.gemfire.GemfireOperations
* @see org.springframework.session.ExpiringSession
* @see org.springframework.session.FindByPrincipalNameSessionRepository
* @see org.springframework.session.FindByIndexNameSessionRepository
* @see org.springframework.session.Session
* @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration
* @see com.gemstone.gemfire.cache.Region
@@ -77,7 +77,7 @@ import com.gemstone.gemfire.cache.util.CacheListenerAdapter;
* @since 1.1.0
*/
public abstract class AbstractGemFireOperationsSessionRepository extends CacheListenerAdapter<Object, ExpiringSession>
implements InitializingBean, FindByPrincipalNameSessionRepository<ExpiringSession>,
implements InitializingBean, FindByIndexNameSessionRepository<ExpiringSession>,
ApplicationEventPublisherAware {
private int maxInactiveIntervalInSeconds = GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS;
@@ -458,12 +458,12 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
/* (non-Javadoc) */
public synchronized void setPrincipalName(String principalName) {
setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
}
/* (non-Javadoc) */
public synchronized String getPrincipalName() {
return getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
return getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
}
/* (non-Javadoc) */

View File

@@ -16,6 +16,7 @@
package org.springframework.session.data.gemfire;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -55,13 +56,17 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation
/**
* Looks up all the available Sessions tied to the specific user identified by principal name.
*
* @param principalName the principal name (i.e. username) to search for all existing Spring Sessions.
* @param indexName the name of the indexed value (i.e. FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME).
* @param indexValue the value of the index to search for (i.e. username) to search for all existing Spring Sessions.
* @return a mapping of Session ID to Session instances.
* @see org.springframework.session.ExpiringSession
*/
public Map<String, ExpiringSession> findByPrincipalName(String principalName) {
public Map<String, ExpiringSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if(!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
SelectResults<ExpiringSession> results = getTemplate().find(String.format(
FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()), principalName);
FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()), indexValue);
Map<String, ExpiringSession> sessions = new HashMap<String, ExpiringSession>(results.size());

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.session.data.redis;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -33,9 +34,11 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
@@ -246,7 +249,7 @@ import org.springframework.util.Assert;
*
* @author Rob Winch
*/
public class RedisOperationsSessionRepository implements FindByPrincipalNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
/**
@@ -358,8 +361,11 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
return getSession(id, false);
}
public Map<String,RedisSession> findByPrincipalName(String principalName) {
String principalKey = getPrincipalKey(principalName);
public Map<String,RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if(!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
String principalKey = getPrincipalKey(indexValue);
Set<Object> sessionIds = sessionRedisOperations.boundSetOps(principalKey).members();
Map<String,RedisSession> sessions = new HashMap<String,RedisSession>(sessionIds.size());
for(Object id : sessionIds) {
@@ -469,7 +475,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
String principal = (String) session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
String principal = (String) session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if(principal != null) {
sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
@@ -530,7 +536,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
}
String getPrincipalKey(String principalName) {
return this.keyPrefix + "index:" + Session.PRINCIPAL_NAME_ATTRIBUTE_NAME + ":" + principalName;
return this.keyPrefix + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + principalName;
}
String getExpirationsKey(long expiration) {
@@ -623,7 +629,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
RedisSession(MapSession cached) {
Assert.notNull("MapSession cannot be null");
this.cached = cached;
this.originalPrincipalName = cached.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
this.originalPrincipalName = cached.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
}
public void setNew(boolean isNew) {
@@ -689,7 +695,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
private void saveDelta() {
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(delta);
String key = getSessionAttrNameKey(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
String key = getSessionAttrNameKey(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if(delta.containsKey(key)) {
if(originalPrincipalName != null) {
String originalPrincipalKey = getPrincipalKey((String) originalPrincipalName);
@@ -709,4 +715,30 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
}
class PrincipalNameResolver {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String NO_VALUE = "org.springframework.session.data.redis.$PrincipalNameResolver.NO_VALUE";
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Map<String,Object> delta) {
String key = getSessionAttrNameKey(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if(delta.containsKey(key)) {
Object principal = delta.get(key);
return (String) principal;
}
key = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if(delta.containsKey(key)) {
Object authentication = delta.get(key);
if(authentication == null) {
return null;
}
Expression expression = parser.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return NO_VALUE;
}
}
}

View File

@@ -62,6 +62,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration;
import org.springframework.session.events.AbstractSessionEvent;
@@ -757,16 +758,16 @@ public class AbstractGemFireOperationsSessionRepositoryTest {
session.setPrincipalName("jblum");
assertThat(session.getPrincipalName()).isEqualTo("jblum");
assertThat(session.getAttributeNames()).isEqualTo(asSet(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME));
assertThat(String.valueOf(session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME))).isEqualTo("jblum");
assertThat(session.getAttributeNames()).isEqualTo(asSet(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME));
assertThat(String.valueOf(session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))).isEqualTo("jblum");
session.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, "rwinch");
session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "rwinch");
assertThat(session.getAttributeNames()).isEqualTo(asSet(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME));
assertThat(String.valueOf(session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME))).isEqualTo("rwinch");
assertThat(session.getAttributeNames()).isEqualTo(asSet(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME));
assertThat(String.valueOf(session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))).isEqualTo("rwinch");
assertThat(session.getPrincipalName()).isEqualTo("rwinch");
session.removeAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
session.removeAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
assertThat(session.getPrincipalName()).isNull();
}
@@ -829,7 +830,7 @@ public class AbstractGemFireOperationsSessionRepositoryTest {
session.fromData(mockDataInput);
Set<String> expectedAttributeNames = asSet("attrOne", "attrTwo", Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
Set<String> expectedAttributeNames = asSet("attrOne", "attrTwo", FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
assertThat(session.getId()).isEqualTo("2");
assertThat(session.getCreationTime()).isEqualTo(expectedCreationTime);
@@ -840,7 +841,7 @@ public class AbstractGemFireOperationsSessionRepositoryTest {
assertThat(session.getAttributeNames().containsAll(expectedAttributeNames)).isTrue();
assertThat(String.valueOf(session.getAttribute("attrOne"))).isEqualTo("testOne");
assertThat(String.valueOf(session.getAttribute("attrTwo"))).isEqualTo("testTwo");
assertThat(String.valueOf(session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME)))
assertThat(String.valueOf(session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME)))
.isEqualTo(expectedPrincipalName);
verify(mockDataInput, times(2)).readUTF();
@@ -1311,7 +1312,7 @@ public class AbstractGemFireOperationsSessionRepositoryTest {
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(60);
assertThat(session.getPrincipalName()).isEqualTo("jblum");
assertThat(session.getAttributeNames().size()).isEqualTo(1);
assertThat(String.valueOf(session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME))).isEqualTo("jblum");
assertThat(String.valueOf(session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))).isEqualTo("jblum");
session.setAttribute("tennis", "ping");
session.setAttribute("junk", "test");
@@ -1374,7 +1375,7 @@ public class AbstractGemFireOperationsSessionRepositoryTest {
super(gemfireOperations);
}
public Map<String, ExpiringSession> findByPrincipalName(String principalName) {
public Map<String, ExpiringSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
throw new UnsupportedOperationException("not implemented");
}

View File

@@ -47,6 +47,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.gemfire.GemfireAccessor;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionDeletedEvent;
@@ -131,7 +132,7 @@ public class GemFireOperationsSessionRepositoryTest {
when(mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults);
Map<String, ExpiringSession> sessions = sessionRepository.findByPrincipalName(principalName);
Map<String, ExpiringSession> sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
assertThat(sessions).isNotNull();
assertThat(sessions.size()).isEqualTo(3);
@@ -160,7 +161,7 @@ public class GemFireOperationsSessionRepositoryTest {
when(mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults);
Map<String, ExpiringSession> sessions = sessionRepository.findByPrincipalName(principalName);
Map<String, ExpiringSession> sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
assertThat(sessions).isNotNull();
assertThat(sessions.isEmpty()).isTrue();

View File

@@ -51,8 +51,14 @@ import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.PrincipalNameResolver;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent;
@@ -345,7 +351,7 @@ public class RedisOperationsSessionRepositoryTests {
LAST_ACCESSED_ATTR, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
when(boundHashOperations.entries()).thenReturn(map);
assertThat(redisRepository.findByPrincipalName("principal")).isEmpty();
assertThat(redisRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "principal")).isEmpty();
}
@Test
@@ -363,7 +369,7 @@ public class RedisOperationsSessionRepositoryTests {
LAST_ACCESSED_ATTR, lastAccessed);
when(boundHashOperations.entries()).thenReturn(map);
Map<String, RedisSession> sessionIdToSessions = redisRepository.findByPrincipalName("principal");
Map<String, RedisSession> sessionIdToSessions = redisRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "principal");
assertThat(sessionIdToSessions).hasSize(1);
RedisSession session = sessionIdToSessions.get(sessionId);