How to destroy multiple sessions using SessionRegistry [duplicate] - java

I have to solve the following scenario, in a Spring Security 3.2.5-RELEASE with Spring Core 4.1.2-RELEASE application running Java 1.7 on wildfly 8.1.
user 'bob' logs in
and Admin deletes 'bob'
if 'bob' logs out, he can't log in. again but he`s current session remains active.
i want to kick 'bob' out
//this doesn't work
for (final SessionInformation session : sessionRegistry.getAllSessions(user, true)) {
session.expireNow();
}

add application event listener to track HttpSessionCreatedEvent and HttpSessionDestroyedEvent and register it as an ApplicationListener and maintain a cache of SessionId to HttoSession.
(optional) add your own ApplicationEvent class AskToExpireSessionEvent -
in you user management service add dependencies to SessionRegistry and ApplicationEventPublisher so that you could list through the currently active user sessions and find the ones (cause there could be many) which are active for the user you are looking for i.e. 'bob'
when deleting a user dispatch an AskToExpireSessionEvent for each of his sessions.
use a weak reference HashMap to track the sessions
user service:
#Service
public class UserServiceImpl implements UserService {
/** {#link SessionRegistry} does not exists in unit tests */
#Autowired(required = false)
private Set<SessionRegistry> sessionRegistries;
#Autowired
private ApplicationEventPublisher publisher;
/**
* destroys all active sessions.
* #return <code>true</code> if any session was invalidated^
* #throws IllegalArgumentException
*/
#Override
public boolean invalidateUserByUserName(final String userName) {
if(null == StringUtils.trimToNull(userName)) {
throw new IllegalArgumentException("userName must not be null or empty");
}
boolean expieredAtLeastOneSession = false;
for (final SessionRegistry sessionRegistry : safe(sessionRegistries)) {
findPrincipal: for (final Object principal : sessionRegistry.getAllPrincipals()) {
if(principal instanceof IAuthenticatedUser) {
final IAuthenticatedUser user = (IAuthenticatedUser) principal;
if(userName.equals(user.getUsername())) {
for (final SessionInformation session : sessionRegistry.getAllSessions(user, true)) {
session.expireNow();
sessionRegistry.removeSessionInformation(session.getSessionId());
publisher.publishEvent(AskToExpireSessionEvent.of(session.getSessionId()));
expieredAtLeastOneSession = true;
}
break findPrincipal;
}
} else {
logger.warn("encountered a session for a none user object {} while invalidating '{}' " , principal, userName);
}
}
}
return expieredAtLeastOneSession;
}
}
Application event:
import org.springframework.context.ApplicationEvent;
public class AskToExpireSessionEvent extends ApplicationEvent {
private static final long serialVersionUID = -1915691753338712193L;
public AskToExpireSessionEvent(final Object source) {
super(source);
}
#Override
public String getSource() {
return (String)super.getSource();
}
public static AskToExpireSessionEvent of(final String sessionId) {
return new AskToExpireSessionEvent(sessionId);
}
}
http session caching listener:
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
import org.springframework.stereotype.Component;
import com.cb4.base.service.event.AskToExpireSessionEvent;
#Component
public class HttpSessionCachingListener {
private static final Logger logger = LoggerFactory.getLogger(HttpSessionCachingListener.class);
private final Map<String, HttpSession> sessionCache = new WeakHashMap<>();
void onHttpSessionCreatedEvent(final HttpSessionCreatedEvent event){
if (event != null && event.getSession() != null && event.getSession().getId() != null) {
sessionCache.put(event.getSession().getId(), event.getSession());
}
}
void onHttpSessionDestroyedEvent(final HttpSessionDestroyedEvent event){
if (event != null && event.getSession() != null && event.getSession().getId() != null){
sessionCache.remove(event.getSession().getId());
}
}
public void timeOutSession(final String sessionId){
if(sessionId != null){
final HttpSession httpSession = sessionCache.get(sessionId);
if(null != httpSession){
logger.debug("invalidating session {} in 1 second", sessionId);
httpSession.setMaxInactiveInterval(1);
}
}
}
#Component
static class HttpSessionCreatedLisener implements ApplicationListener<HttpSessionCreatedEvent> {
#Autowired
HttpSessionCachingListener parent;
#Override
public void onApplicationEvent(final HttpSessionCreatedEvent event) {
parent.onHttpSessionCreatedEvent(event);
}
}
#Component
static class HttpSessionDestroyedLisener implements ApplicationListener<HttpSessionDestroyedEvent> {
#Autowired
HttpSessionCachingListener parent;
#Override
public void onApplicationEvent(final HttpSessionDestroyedEvent event) {
parent.onHttpSessionDestroyedEvent(event);
}
}
#Component
static class AskToTimeOutSessionLisener implements ApplicationListener<AskToExpireSessionEvent> {
#Autowired
HttpSessionCachingListener parent;
#Override
public void onApplicationEvent(final AskToExpireSessionEvent event) {
if(event != null){
parent.timeOutSession(event.getSource());
}
}
}
}

Using java config add the following code in your class extending WebSecurityConfigurerAdapter :
#Bean
public SessionRegistry sessionRegistry( ) {
SessionRegistry sessionRegistry = new SessionRegistryImpl( );
return sessionRegistry;
}
#Bean
public RegisterSessionAuthenticationStrategy registerSessionAuthStr( ) {
return new RegisterSessionAuthenticationStrategy( sessionRegistry( ) );
}
and add the following in your configure( HttpSecurity http ) method:
http.sessionManagement( ).maximumSessions( -1 ).sessionRegistry( sessionRegistry( ) );
http.sessionManagement( ).sessionFixation( ).migrateSession( )
.sessionAuthenticationStrategy( registerSessionAuthStr( ) );
Also, set the registerSessionAuthenticationStratergy in your custom authentication bean as follows:
usernamePasswordAuthenticationFilter
.setSessionAuthenticationStrategy( registerSessionAuthStr( ) );
NOTE: Setting registerSessionAuthenticationStratergy in your custom authentication bean causes the prinicpal list to be populated and hence when you try to fetch the list of all prinicipals from sessionRegistry ( sessionRegistry.getAllPrinicpals() ), the list is NOT empty.

Related

why does my vue client not receive websocket messages

I want to send messages via queue and topic to my frontend but it doesnt work, useing SOCKJS and STOMP. My backend logs, that new memeber subscribe, but when i send a message via convertAndSend or convertAndSendToUser nothing happens on the client side. pls help me. ty in advance. pls dont delete this question.
SERVER:
CONFIG:
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/user", "/topic", "/queue");
config.setApplicationDestinationPrefixes("/ws");
}
#Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.interceptors(new UserInterceptor());
}
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOriginPatterns("*").withSockJS();
}
CONTROLLER:
#Controller
#Slf4j
public class WebsocketController {
#Autowired
SimpMessagingTemplate simpMessagingTemplate;
#Autowired
LobbyManagerService lobbyManagerService;
#Autowired
WebsocketService websocketService;
final String newMember = "/queue/newMember/";
final String lobbyDestination = "/topic/lobby/";
#MessageMapping("/get/infos/on/join/{lobby}")
public void getInfosOnJoinLobby(final SimpMessageHeaderAccessor sha, #DestinationVariable final String lobby) throws JsonProcessingException {
log.info("Send lobby infos to newly joined player");
final Message joinLobbyMessage = new JoinLobbyMessage(lobbyManagerService.getLobby(lobby).getPlayerNames());
final MessageWrapper joinLobbyMessageWrapped = websocketService.wrapMessage(joinLobbyMessage, Purpose.JOIN_LOBBY_MESSAGE);
simpMessagingTemplate.convertAndSendToUser(sha.getUser().getName(), newMember + lobby, joinLobbyMessageWrapped);
}
#MessageMapping("/lobby/{lobby}/join/team/{team}/player/{player}")
public void joinTeam(#DestinationVariable final String lobby, #DestinationVariable final String team, #DestinationVariable final String player) throws JsonProcessingException {
log.info("player '{}' joined team '{}' in lobby '{}'", player, team, lobby);
final JoinTeamMessage joinTeamMessage = new JoinTeamMessage(team, player);
final MessageWrapper joinTeamMessageWrapped = websocketService.wrapMessage(joinTeamMessage, Purpose.JOIN_TEAM_MESSAGE);
simpMessagingTemplate.convertAndSend(lobbyDestination + lobby, joinTeamMessageWrapped);
}
#MessageMapping("/start/lobby/{lobby}")
public void startLobby(#DestinationVariable final String lobby) {
log.info("start lobby");
//todo this methods will be implemented later
}
}
CLIENT:
function connect() {
console.log("connect to lobby");
return new Promise((resolve, reject) => {
stompClientGame.value = Stomp.over(
new SockJS("/minigames/towercrush/api/v1/connect")
);
let uuid = generateUUID();
console.log("players uuid was: ", uuid);
stompClientGame.value.connect(
{ player: player.value, lobby: lobby.value, userUUID: uuid },
() => resolve(stompClientGame.value)
);
});
}
function connectToLobby() {
connect()
.then(() => {
stompClientGame.value.subscribe(
"/user/queue/newMember/" + lobby.value,
function (messageOutput: any) {
handleMessageReceipt(messageOutput.body);
}
);
})
.then(() => {
stompClientGame.value.subscribe(
"/topic/lobby/" + lobby.value,
function (messageOutput: any) {
handleMessageReceipt(messageOutput.body);
}
);
});
}
i tryed literally every possible variation that came to my mind but nothing worked. pls help.

java.lang.IllegalStateException: No user is currently signed in

I have been try to integrate social login in my spring application. Previously I had encountered errors like no bean name facebook and connectionRepository. Once I integrated code from https://github.com/spring-social/spring-social-google-example, specially socialConfig and other files , I was able to move forward. But still I get error, java.lang.IllegalStateException: No user is currently signed in, which exists on SecurityContext.java file where the getCurrentUser() method throws IllegalStateException, Even though I have set the currentUser from the SimpleSignInAdapter.signIn method.
For facebook user i have used the org.springframework.social.facebook.api.User and set SecurityContext.setCurrentUser(new User(userId, null, null, null, null, null));
Error occurs from ConnectionRepository.connectionRepository while calling User user = SecurityContext.getCurrentUser(). I have printed the value and it returns null and exception is thrown. I havent used threadlocal before and having difficulties working it. Is there another alternatives for threadlocal and any other possible solution ?
My SecurityContext class is as below
public final class SecurityContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<User>();
private static Logger logger = LoggerFactory.getLogger(SecurityContext.class);
public static User getCurrentUser() {
User user = currentUser.get();
if (user == null) {
throw new IllegalStateException("No user is currently signed in");
}
return user;
}
public static void setCurrentUser(User user) {
currentUser.set(user);
}
public static boolean userSignedIn() {
return currentUser.get() != null;
}
public static void remove() {
currentUser.remove();
}
}
The ThreadLocal currentUser is set from following class
public final class SimpleSignInAdapter implements SignInAdapter {
private final UserCookieGenerator userCookieGenerator = new UserCookieGenerator();
public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
User user= new User(userId, null, null, null, null, null);
SecurityContext.setCurrentUser(user);
userCookieGenerator.addCookie(userId, request.getNativeResponse(HttpServletResponse.class));
boolean flag = true;
if (flag) {
return "/signup";
}
return null;
}
}
Below is my connectionRepository method from socialConfig.class from where the error occurs while calling SecurityContext.getCurrentUser() which is null even thoogh the currentuser is set in above class.
#Bean
#Scope(value="request", proxyMode=ScopedProxyMode.INTERFACES)
public ConnectionRepository connectionRepository() {
User user = SecurityContext.getCurrentUser();
ConnectionRepositorycr=usersConnectionRepository().createConnectionRepository(user.getId());
logger.debug("cr==>"+cr);
return cr;
}

Dynamic Multi-tenant WebApp (Spring Hibernate)

I've come up with a working dynamic multi-tenant application using:
Java 8
Java Servlet 3.1
Spring 3.0.7-RELEASE (can't change the version)
Hibernate 3.6.0.Final (can't change the version)
Commons dbcp2
This is the 1st time I've had to instantiate Spring objects myself so I'm wondering if I've done everything correctly or if the app will blow up in my face at an unspecified future date during production.
Basically, the single DataBase schema is known, but the database details will be specified at runtime by the user. They are free to specify any hostname/port/DB name/username/password.
Here's the workflow:
The user logs in to the web app then either chooses a database from a known list, or specifies a custom database (hostname/port/etc.).
If the Hibernate SessionFactory is built successfully (or is found in the cache), then it's persisted for the user's session using SourceContext#setSourceId(SourceId) then the user can work with this database.
If anybody choses/specifies the same database, the same cached AnnotationSessionFactoryBean is returned
The user can switch databases at any point.
When the user switches away from a custom DB (or logs off), the cached AnnotationSessionFactoryBeans are removed/destroyed
So will the following work as intended? Help and pointers are most welcome.
web.xml
<web-app version="3.1" ...>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener> <!-- Needed for SourceContext -->
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<web-app>
applicationContext.xml
<beans ...>
<tx:annotation-driven />
<util:properties id="db" location="classpath:db.properties" /> <!-- driver/url prefix -->
<context:component-scan base-package="com.example.basepackage" />
</beans>
UserDao.java
#Service
public class UserDao implements UserDaoImpl {
#Autowired
private TemplateFactory templateFactory;
#Override
public void addTask() {
final HibernateTemplate template = templateFactory.getHibernateTemplate();
final User user = (User) DataAccessUtils.uniqueResult(
template.find("select distinct u from User u left join fetch u.tasks where u.id = ?", 1)
);
final Task task = new Task("Do something");
user.getTasks().add(task);
TransactionTemplate txTemplate = templateFactory.getTxTemplate(template);
txTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.save(task);
template.update(user);
}
});
}
}
TemplateFactory.java
#Service
public class TemplateFactory {
#Autowired
private SourceSessionFactory factory;
#Resource(name = "SourceContext")
private SourceContext srcCtx; // session scope, proxied bean
#Override
public HibernateTemplate getHibernateTemplate() {
LocalSessionFactoryBean sessionFactory = factory.getSessionFactory(srcCtx.getSourceId());
return new HibernateTemplate(sessionFactory.getObject());
}
#Override
public TransactionTemplate getTxTemplate(HibernateTemplate template) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(template.getSessionFactory());
return new TransactionTemplate(txManager);
}
}
SourceContext.java
#Component("SourceContext")
#Scope(value="session", proxyMode = ScopedProxyMode.INTERFACES)
public class SourceContext {
private static final long serialVersionUID = -124875L;
private SourceId id;
#Override
public SourceId getSourceId() {
return id;
}
#Override
public void setSourceId(SourceId id) {
this.id = id;
}
}
SourceId.java
public interface SourceId {
String getHostname();
int getPort();
String getSID();
String getUsername();
String getPassword();
// concrete class has proper hashCode/equals/toString methods
// which use all of the SourceIds properties above
}
SourceSessionFactory.java
#Service
public class SourceSessionFactory {
private static Map<SourceId, AnnotationSessionFactoryBean> cache = new HashMap<SourceId, AnnotationSessionFactoryBean>();
#Resource(name = "db")
private Properties db;
#Override
public LocalSessionFactoryBean getSessionFactory(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.get(id);
if (sessionFactory == null) {
return createSessionFactory(id);
}
else {
return sessionFactory;
}
}
}
private AnnotationSessionFactoryBean createSessionFactory(SourceId id) {
AnnotationSessionFactoryBean sessionFactory = new AnnotationSessionFactoryBean();
sessionFactory.setDataSource(new CutomDataSource(id, db));
sessionFactory.setPackagesToScan(new String[] { "com.example.basepackage" });
try {
sessionFactory.afterPropertiesSet();
}
catch (Exception e) {
throw new SourceException("Unable to build SessionFactory for:" + id, e);
}
cache.put(id, sessionFactory);
return sessionFactory;
}
public void destroy(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.remove(id);
if (sessionFactory != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Releasing SessionFactory for: " + id);
}
try {
sessionFactory.destroy();
}
catch (HibernateException e) {
LOG.error("Unable to destroy SessionFactory for: " + id);
e.printStackTrace(System.err);
}
}
}
}
}
CustomDataSource.java
public class CutomDataSource extends BasicDataSource { // commons-dbcp2
public CutomDataSource(SourceId id, Properties db) {
setDriverClassName(db.getProperty("driverClassName"));
setUrl(db.getProperty("url") + id.getHostname() + ":" + id.getPort() + ":" + id.getSID());
setUsername(id.getUsername());
setPassword(id.getPassword());
}
}
In the end I extended Spring's AbstractRoutingDataSource to be able to dynamically create datasources on the fly. I'll update this answer with the full code as soon as everything is working correctly. I have a couple of last things to sort out, but the crux of it is as follows:
#Service
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// this is pretty much the same as the above SourceSessionFactory
// but with a map of CustomDataSources instead of
// AnnotationSessionFactoryBeans
#Autowired
private DynamicDataSourceFactory dataSourceFactory;
// This is the sticky part. I currently have a workaround instead.
// Hibernate needs an actual connection upon spring startup & there's
// also no session in place during spring initialization. TBC.
// #Resource(name = "UserContext") // scope session, proxy bean
private UserContext userCtx; // something that returns the DB config
#Override
protected SourceId determineCurrentLookupKey() {
return userCtx.getSourceId();
}
#Override
protected CustomDataSource determineTargetDataSource() {
SourceId id = determineCurrentLookupKey();
return dataSourceFactory.getDataSource(id);
}
#Override
public void afterPropertiesSet() {
// we don't need to resolve any data sources
}
// Inherited methods copied here to show what's going on
// #Override
// public Connection getConnection() throws SQLException {
// return determineTargetDataSource().getConnection();
// }
//
// #Override
// public Connection getConnection(String username, String password)
// throws SQLException {
// return determineTargetDataSource().getConnection(username, password);
// }
}
So I just wire up the DynamicRoutingDataSource as the DataSource for Spring's SessionFactoryBean along with a TransactionManager an all the rest as usual. As I said, more code to follow.

Set SNS WaitTime in Spring AWS Cloud Framework

I am using Spring AWS Cloud Framework to poll for S3 Event notifications on a queue. I am using the QueueMessagingTemplate to do this. I want to be able to set the max number of messages and wait time to poll see: http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html.
import com.amazonaws.services.s3.event.S3EventNotification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate;
public class MyQueue {
private QueueMessagingTemplate queueMsgTemplate;
#Autowired
public MyQueue(QueueMessagingTemplate queueMsgTemplate) {
this.queueMsgTemplate = queueMsgTemplate;
}
#Override
public S3EventNotification poll() {
S3EventNotification s3Event = queueMsgTemplate
.receiveAndConvert("myQueueName", S3EventNotification.class);
}
}
Context
#Bean
public AWSCredentialsProviderChain awsCredentialsProviderChain() {
return new AWSCredentialsProviderChain(
new DefaultAWSCredentialsProviderChain());
}
#Bean
public ClientConfiguration clientConfiguration() {
return new ClientConfiguration();
}
#Bean
public AmazonSQS sqsClient(ClientConfiguration clientConfiguration,// UserData userData,
AWSCredentialsProviderChain credentialsProvider) {
AmazonSQSClient amazonSQSClient = new AmazonSQSClient(credentialsProvider, clientConfiguration);
amazonSQSClient.setEndpoint("http://localhost:9324");
return amazonSQSClient;
}
#Bean
public QueueMessagingTemplate queueMessagingTemplate(AmazonSQS sqsClient) {
return new QueueMessagingTemplate(sqsClient);
}
Any idea how to configure these? Thanks
The QueueMessagingTemplate.receiveAndConvert() is based on the QueueMessageChannel.receive() method where you can find a desired code:
#Override
public Message<String> receive() {
return this.receive(0);
}
#Override
public Message<String> receive(long timeout) {
ReceiveMessageResult receiveMessageResult = this.amazonSqs.receiveMessage(
new ReceiveMessageRequest(this.queueUrl).
withMaxNumberOfMessages(1).
withWaitTimeSeconds(Long.valueOf(timeout).intValue()).
withAttributeNames(ATTRIBUTE_NAMES).
withMessageAttributeNames(MESSAGE_ATTRIBUTE_NAMES));
if (receiveMessageResult.getMessages().isEmpty()) {
return null;
}
com.amazonaws.services.sqs.model.Message amazonMessage = receiveMessageResult.getMessages().get(0);
Message<String> message = createMessage(amazonMessage);
this.amazonSqs.deleteMessage(new DeleteMessageRequest(this.queueUrl, amazonMessage.getReceiptHandle()));
return message;
}
So, as you see withMaxNumberOfMessages(1) is hardcoded to 1. And that is correct because receive() can poll only one message. The withWaitTimeSeconds(Long.valueOf(timeout).intValue()) is fully equals to the provided timeout. And eh, you can't modify it in case of receiveAndConvert().
Consider to use QueueMessageChannel.receive(long timeout) and messageConverter from the QueueMessagingTemplate. Like it is done in the:
public <T> T receiveAndConvert(QueueMessageChannel destination, Class<T> targetClass) throws MessagingException {
Message<?> message = destination.receive();
if (message != null) {
return (T) getMessageConverter().fromMessage(message, targetClass);
} else {
return null;
}
}
You can reach an appropriate QueueMessageChannel via code:
String physicalResourceId = this.destinationResolver.resolveDestination(destination);
new QueueMessageChannel(this.amazonSqs, physicalResourceId);

Multiple sessions for one servlet in Java

I have one servlet taking care of multiple sites and therefore I want to have different sessions for different sites, even if its the same user.
Is there any support for this in Java or do I need to prefix the attribute names instead? I guess prefixing is not a good idea.
/Br Johannes
This CANNOT be done in the servlet container based on URL parameters alone; you'll have to do it yourself. Instead of dealing with attribute prefixes in your servlet, however, the easiest way to manage "separate" sessions is via filter:
Write a simple wrapper class for HttpSession. Have it hold a Map of attributes and back all attribute / value methods by said map; delegate all the other methods to the actual session you're wrapping. Override invalidate() method to remove your session wrapper instead of killing the entire "real" session.
Write a servlet filter; map it to intercept all applicable URLs.
Maintain a collection of your session wrappers as an attribute within the real session.
In your filter's doFilter() method extract the appropriate session wrapper from the collection and inject it into the request you're passing down the chain by wrapping the original request into HttpServletRequestWrapper whose getSession() method is overwritten.
Your servlets / JSPs / etc... will enjoy "separate" sessions.
Note that Sessions's "lastAccessedTime" is shared with this approach. If you need to keep those separate you'll have to write your own code for maintaining this setting and for expiring your session wrappers.
I recently came across this problem too, and I went with ChssPly76's suggestion to solve it. I thought I'd post my results here to provide a reference implementation. It hasn't been extensively tested, so kindly let me know if you spot any weaknesses.
I assume that every request to a servlet contains a parameter named uiid, which represents a user ID. The requester has to keep track of sending a new ID everytime a link is clicked that opens a new window. In my case this is sufficient, but feel free to use any other (maybe more secure) method here. Furthermore, I work with Tomcat 7 or 8. You might need to extend other classes when working with different servlet containers, but the APIs shouldn't change too much.
In the following, the created sessions are referred to as subsessions, the original container managed session is the parent session. The implementation consists of the following five classes:
The SingleSessionManager keeps track of creation, distribution and cleanup of all subsessions. It does this by acting as a servlet filter which replaces the ServletRequest with a wrapper that returns the appropriate subsession. A scheduler periodically checks for expired subsessions ...and yes, it's a singleton. Sorry, but I still like them.
package session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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.HttpSession;
/**
* A singleton class that manages multiple sessions on top of a regular container managed session.
* See web.xml for information on how to enable this.
*
*/
public class SingleSessionManager implements Filter {
/**
* The default session timeout in seconds to be used if no explicit timeout is provided.
*/
public static final int DEFAULT_TIMEOUT = 900;
/**
* The default interval for session validation checks in seconds to be used if no explicit
* timeout is provided.
*/
public static final int DEFAULT_SESSION_INVALIDATION_CHECK = 15;
private static SingleSessionManager instance;
private ScheduledExecutorService scheduler;
protected int timeout;
protected long sessionInvalidationCheck;
private Map<SubSessionKey, HttpSessionWrapper> sessions = new ConcurrentHashMap<SubSessionKey, HttpSessionWrapper>();
public SingleSessionManager() {
sessionInvalidationCheck = DEFAULT_SESSION_INVALIDATION_CHECK;
timeout = DEFAULT_TIMEOUT;
}
public static SingleSessionManager getInstance() {
if (instance == null) {
instance = new SingleSessionManager();
}
return instance;
}
#Override
public void destroy() {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrapper, response);
}
#Override
public void init(FilterConfig cfg) throws ServletException {
String timeout = cfg.getInitParameter("sessionTimeout");
if (timeout != null && !timeout.trim().equals("")) {
getInstance().timeout = Integer.parseInt(timeout) * 60;
}
String sessionInvalidationCheck = cfg.getInitParameter("sessionInvalidationCheck");
if (sessionInvalidationCheck != null && !sessionInvalidationCheck.trim().equals("")) {
getInstance().sessionInvalidationCheck = Long.parseLong(sessionInvalidationCheck);
}
getInstance().startSessionExpirationScheduler();
}
/**
* Create a new session ID.
*
* #return A new unique session ID.
*/
public String generateSessionId() {
return UUID.randomUUID().toString();
}
protected void startSessionExpirationScheduler() {
if (scheduler == null) {
scheduler = Executors.newScheduledThreadPool(1);
final Runnable sessionInvalidator = new Runnable() {
public void run() {
SingleSessionManager.getInstance().destroyExpiredSessions();
}
};
final ScheduledFuture<?> sessionInvalidatorHandle =
scheduler.scheduleAtFixedRate(sessionInvalidator
, this.sessionInvalidationCheck
, this.sessionInvalidationCheck
, TimeUnit.SECONDS);
}
}
/**
* Get the timeout after which a session will be invalidated.
*
* #return The timeout of a session in seconds.
*/
public int getSessionTimeout() {
return timeout;
}
/**
* Retrieve a session.
*
* #param uiid
* The user id this session is to be associated with.
* #param create
* If <code>true</code> and no session exists for the given user id, a new session is
* created and associated with the given user id. If <code>false</code> and no
* session exists for the given user id, no new session will be created and this
* method will return <code>null</code>.
* #param originalSession
* The original backing session created and managed by the servlet container.
* #return The session associated with the given user id if this session exists and/or create is
* set to <code>true</code>, <code>null</code> otherwise.
*/
public HttpSession getSession(String uiid, boolean create, HttpSession originalSession) {
if (uiid != null) {
SubSessionKey key = new SubSessionKey(originalSession.getId(), uiid);
if (!sessions.containsKey(key) && create) {
HttpSessionWrapper sw = new HttpSessionWrapper(uiid, originalSession);
sessions.put(key, sw);
}
HttpSessionWrapper session = sessions.get(key);
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
return null;
}
public HttpSessionWrapper removeSession(SubSessionKey key) {
return sessions.remove(key);
}
/**
* Destroy a session, freeing all it's resources.
*
* #param session
* The session to be destroyed.
*/
public void destroySession(HttpSessionWrapper session) {
String uiid = ((HttpSessionWrapper)session).getUiid();
SubSessionKey key = new SubSessionKey(session.getOriginalSession().getId(), uiid);
HttpSessionWrapper w = getInstance().removeSession(key);
if (w != null) {
System.out.println("Session " + w.getId() + " with uiid " + uiid + " was destroyed.");
} else {
System.out.println("uiid " + uiid + " does not have a session.");
}
}
/**
* Destroy all session that are expired at the time of this method call.
*/
public void destroyExpiredSessions() {
List<HttpSessionWrapper> markedForDelete = new ArrayList<HttpSessionWrapper>();
long time = System.currentTimeMillis() / 1000;
for (HttpSessionWrapper session : sessions.values()) {
if (time - (session.getLastAccessedTime() / 1000) >= session.getMaxInactiveInterval()) {
markedForDelete.add(session);
}
}
for (HttpSessionWrapper session : markedForDelete) {
destroySession(session);
}
}
/**
* Remove all subsessions that were created from a given parent session.
*
* #param originalSession
* All subsessions created with this session as their parent session will be
* invalidated.
*/
public void clearAllSessions(HttpSession originalSession) {
Iterator<HttpSessionWrapper> it = sessions.values().iterator();
while (it.hasNext()) {
HttpSessionWrapper w = it.next();
if (w.getOriginalSession().getId().equals(originalSession.getId())) {
destroySession(w);
}
}
}
public void setSessionTimeout(int timeout) {
this.timeout = timeout;
}
}
A subsession is identified by a SubSessionKey. These key objects depend on the uiid and the ID of the parent session.
package session;
/**
* Key object for identifying a subsession.
*
*/
public class SubSessionKey {
private String sessionId;
private String uiid;
/**
* Create a new instance of {#link SubSessionKey}.
*
* #param sessionId
* The session id of the parent session.
* #param uiid
* The users's id this session is associated with.
*/
public SubSessionKey(String sessionId, String uiid) {
super();
this.sessionId = sessionId;
this.uiid = uiid;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode());
result = prime * result + ((uiid == null) ? 0 : uiid.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SubSessionKey other = (SubSessionKey) obj;
if (sessionId == null) {
if (other.sessionId != null)
return false;
} else if (!sessionId.equals(other.sessionId))
return false;
if (uiid == null) {
if (other.uiid != null)
return false;
} else if (!uiid.equals(other.uiid))
return false;
return true;
}
#Override
public String toString() {
return "SubSessionKey [sessionId=" + sessionId + ", uiid=" + uiid + "]";
}
}
The HttpServletRequestWrapper wraps a HttpServletRequest object. All methods are redirected to the wrapped request except for the getSession methods which will return an HttpSessionWrapper depending on the user ID in this request's parameters.
package session;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Wrapper class that wraps a {#link HttpServletRequest} object. All methods are redirected to the
* wrapped request except for the <code>getSession</code> which will return an
* {#link HttpSessionWrapper} depending on the user id in this request's parameters.
*
*/
public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {
private HttpServletRequest req;
public HttpServletRequestWrapper(HttpServletRequest req) {
super(req);
this.req = req;
}
#Override
public HttpSession getSession() {
return getSession(true);
}
#Override
public HttpSession getSession(boolean create) {
String[] uiid = getParameterMap().get("uiid");
if (uiid != null && uiid.length >= 1) {
return SingleSessionManager.getInstance().getSession(uiid[0], create, req.getSession(create));
}
return req.getSession(create);
}
}
The HttpSessionWrapper represents a subsession.
package session;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
/**
* Implementation of a HttpSession. Each instance of this class is created around a container
* managed parent session with it's lifetime linked to it's parent's.
*
*/
#SuppressWarnings("deprecation")
public class HttpSessionWrapper implements HttpSession {
private Map<String, Object> attributes;
private Map<String, Object> values;
private long creationTime;
private String id;
private String uiid;
private boolean isNew;
private long lastAccessedTime;
private HttpSession originalSession;
public HttpSessionWrapper(String uiid, HttpSession originalSession) {
creationTime = System.currentTimeMillis();
lastAccessedTime = creationTime;
id = SingleSessionManager.getInstance().generateSessionId();
isNew = true;
attributes = new HashMap<String, Object>();
Enumeration<String> names = originalSession.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
attributes.put(name, originalSession.getAttribute(name));
}
values = new HashMap<String, Object>();
for (String name : originalSession.getValueNames()) {
values.put(name, originalSession.getValue(name));
}
this.uiid = uiid;
this.originalSession = originalSession;
}
public String getUiid() {
return uiid;
}
public void setNew(boolean b) {
isNew = b;
}
public void setLastAccessedTime(long time) {
lastAccessedTime = time;
}
#Override
public Object getAttribute(String arg0) {
return attributes.get(arg0);
}
#Override
public Enumeration<String> getAttributeNames() {
return Collections.enumeration(attributes.keySet());
}
#Override
public long getCreationTime() {
return creationTime;
}
#Override
public String getId() {
return id;
}
#Override
public long getLastAccessedTime() {
return lastAccessedTime;
}
#Override
public int getMaxInactiveInterval() {
return SingleSessionManager.getInstance().getSessionTimeout();
}
#Override
public ServletContext getServletContext() {
return originalSession.getServletContext();
}
#Override
public HttpSessionContext getSessionContext() {
return new HttpSessionContext() {
#Override
public Enumeration<String> getIds() {
return Collections.enumeration(new HashSet<String>());
}
#Override
public HttpSession getSession(String arg0) {
return null;
}
};
}
#Override
public Object getValue(String arg0) {
return values.get(arg0);
}
#Override
public String[] getValueNames() {
return values.keySet().toArray(new String[values.size()]);
}
#Override
public void invalidate() {
SingleSessionManager.getInstance().destroySession(this);
}
#Override
public boolean isNew() {
return isNew;
}
#Override
public void putValue(String arg0, Object arg1) {
values.put(arg0, arg1);
}
#Override
public void removeAttribute(String arg0) {
attributes.remove(arg0);
}
#Override
public void removeValue(String arg0) {
values.remove(arg0);
}
#Override
public void setAttribute(String arg0, Object arg1) {
attributes.put(arg0, arg1);
}
#Override
public void setMaxInactiveInterval(int arg0) {
SingleSessionManager.getInstance().setSessionTimeout(arg0);
}
public HttpSession getOriginalSession() {
return originalSession;
}
}
The SessionInvalidator is an HttpSessionListener that takes care of cleaning all subsessions in case of the invalidation of their parent session.
package session;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* Session listener that listens for the destruction of a container managed session and takes care
* of destroying all it's subsessions.
* <p>
* Normally this listener won't have much to do since subsessions usually have a shorter lifetime
* than their parent session and therefore will timeout long before this method is called. This
* listener will only be important in case of an explicit invalidation of a parent session.
* </p>
*
*/
public class SessionInvalidator implements HttpSessionListener {
#Override
public void sessionCreated(HttpSessionEvent arg0) {
}
#Override
public void sessionDestroyed(HttpSessionEvent arg0) {
SingleSessionManager.getInstance().clearAllSessions(arg0.getSession());
}
}
Enable everything by putting the following in your web.xml
<filter>
<filter-name>SingleSessionFilter</filter-name>
<filter-class>de.supportgis.sgjWeb.session.SingleSessionManager</filter-class>
<!-- The timeout in minutes after which a subsession will be invalidated. It is recommended to set a session timeout for the servled container using the parameter "session-timeout", which is higher than this value. -->
<init-param>
<param-name>sessionTimeout</param-name>
<param-value>1</param-value>
</init-param>
<init-param>
<!-- The intervall in seconds in which a check for expired sessions will be performed. -->
<param-name>sessionInvalidationCheck</param-name>
<param-value>15</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SingleSessionFilter</filter-name>
<!-- Insert the name of your servlet here to which the session management should apply, or use url-pattern instead. -->
<servlet-name>YourServlet</servlet-name>
</filter-mapping>
<listener>
<listener-class>session.SessionInvalidator</listener-class>
</listener>
<!-- Timeout of the parent session -->
<session-config>
<session-timeout>40</session-timeout>
<!-- Session timeout interval in minutes -->
</session-config>
I think you're looking for something like Apache Tomcat. It will manage individual sessions for individual servlet applications.
The session is unique for a combination of user and web application. You can of course deploy your servlet in several web applications on the same Tomcat instance, but you will not be able to route the HTTP request to different web applications simply based on URL parameters unless you evaluate the URL parameters in a second servlet and redirect the browser to a new URL for the specific web app.
Different servlet containers or J2EE app servers may have different options for routing requests to specific web applications, but AFAIK out of the box, Tomcat can only delegate the request based on either host name or base directory, e.g.:
http://app1/... or http://server/app1/... is delegated to app1
http://app2/... or http://server/app2/... is delegated to app2, and so on
here is a bug fix for user3792852's reply
public HttpSession getSession(String uiid, boolean create, HttpSession originalSession)
{
if (uiid != null && originalSession != null)
{
SubSessionKey key = new SubSessionKey(originalSession.getId(), uiid);
synchronized (sessions)
{
HttpSessionWrapper session = sessions.get(key);
if (session == null && create)
{
session = new HttpSessionWrapper(uiid, originalSession);
sessions.put(key, session);
}
if (session != null)
{
session.setLastAccessedTime(System.currentTimeMillis());
}
return session;
}
}
return null;
}

Categories