You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.
When implementing administrative functions on one's website, a fairly useful function to have is the ability to forcibly expire a user's session out cluster wide. Currently you can expire it out through jmx-console if you know the specific jsessionid, but that is very difficult to know without propper logging and digging through heaps of logs.
This article explores correlating a username with the jsessionid and displaying these values in a dataTable to be easily expired by an administrator.
This example was written for the Enterprise Application Server 4.3 CP04 but the more important version is the JBossCache version which is 1.4.1.
Let's start with the flow of events: 1) User Authenticates and the sessionid and username are inserted into a custom FQN in the TomcatClusteringCache 2) Admin navigates to session view page and expires the session 3) Session is expired through the CacheManager MBean 4) The Session Listener picks up the expired session and removes the sessionid and username in the custom FQN in the TomcatClusteringCache
Following this flow of events I will post the corresponding code
1) Authentication and insertion into TomcatClusteringCache
import javax.faces.context.FacesContext; import javax.management.MBeanServer; import javax.management.MBeanServerInvocationHandler; import javax.management.ObjectName; import javax.servlet.http.HttpSession; import org.domain.example.main.SessionCollectorListener; import org.jboss.cache.TreeCacheMBean; import org.jboss.mx.util.MBeanServerLocator; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Logger; import org.jboss.seam.annotations.Name; import org.jboss.seam.log.Log; import org.jboss.seam.security.Credentials; import org.jboss.seam.security.Identity; @Name("authenticator") public class Authenticator { @Logger private Log log; @In Identity identity; @In Credentials credentials; public boolean authenticate() { log.info("authenticating {0}", credentials.getUsername()); //write your authentication logic here, //return true if the authentication was //successful, false otherwise if ("admin".equals(credentials.getUsername())) { identity.addRole("admin"); try { MBeanServer server=MBeanServerLocator.locateJBoss(); TreeCacheMBean cache; ObjectName objectName = new ObjectName(SessionCollectorListener.CACHE_OBJECT_NAME); cache=(TreeCacheMBean)MBeanServerInvocationHandler.newProxyInstance(server, objectName, TreeCacheMBean.class, false); int identityHashcode = Math.abs(identity.hashCode()); log.info("identity hash: " + identityHashcode); String FQN = SessionCollectorListener.FQN + identityHashcode % 100; String sessionid = ((HttpSession)FacesContext.getCurrentInstance().getExternalContext().getSession(false)).getId(); cache.put(FQN, sessionid, identity.getUsername()); log.info("Adding user: " + identity.getUsername() + " with sessionid: " + sessionid + " to FQN: " + FQN); } catch (Exception e) { e.printStackTrace(); } return true; } return false; } }
The Authenticator is from a default generated Seam project. From that point I calculated the hashcode of the Identity object to use as part of the FQN, then inserted the sessionid and username into the cache using the generated FQN. The reason the hashcode mod 100 is used for the FQN is to prevent 1 node per sessionid, this will save memory in the case of many users.
With the logging in the Authenticator when authenticating you would see the following:
Adding user: admin with sessionid: XoisNwx570QxYG9SBLAEVg**.node1 to FQN: /usersessionmap/example/5
Before moving onto the admin page let's fill in the gaps by looking at the SessionCollectorListener which contains the code for looking up the TreeCacheMBean and CacheManagerMBean.
import java.util.Set; import javax.management.MBeanServer; import javax.management.MBeanServerInvocationHandler; import javax.management.ObjectName; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import org.jboss.cache.Node; import org.jboss.cache.TreeCacheMBean; import org.jboss.mx.util.MBeanServerLocator; import org.jboss.seam.log.LogProvider; import org.jboss.seam.log.Logging; import org.jboss.web.tomcat.service.session.JBossCacheManagerMBean; public class SessionCollectorListener implements ServletContextListener, HttpSessionListener { private static final LogProvider log = Logging.getLogProvider(SessionCollectorListener.class); public static final String WEBAPP_NAME = "example"; public static final String FQN = "/usersessionmap/" + SessionCollectorListener.WEBAPP_NAME + "/"; public static final String TREECACHE_FQN = "/JSESSION/localhost/" + WEBAPP_NAME; public static final String CACHE_OBJECT_NAME = "jboss.cache:service=TomcatClusteringCache"; public static final String CACHE_MANAGER_OBJECT_NAME = "jboss.web:service=ClusterManager,WebModule=//localhost/" + WEBAPP_NAME; public static TreeCacheMBean getTreeCacheMBean() { TreeCacheMBean cache = null; try { MBeanServer server=MBeanServerLocator.locateJBoss(); ObjectName objectName = new ObjectName(SessionCollectorListener.CACHE_OBJECT_NAME); cache=(TreeCacheMBean)MBeanServerInvocationHandler.newProxyInstance(server, objectName, TreeCacheMBean.class, false); } catch (Exception e) { e.printStackTrace(); } return cache; } public static JBossCacheManagerMBean getJBossCacheManager() { JBossCacheManagerMBean cache = null; try { MBeanServer server=MBeanServerLocator.locateJBoss(); ObjectName objectName = new ObjectName(SessionCollectorListener.CACHE_MANAGER_OBJECT_NAME); cache=(JBossCacheManagerMBean)MBeanServerInvocationHandler.newProxyInstance(server, objectName, JBossCacheManagerMBean.class, false); } catch (Exception e) { e.printStackTrace(); } return cache; } public void contextInitialized(ServletContextEvent e) {} public void contextDestroyed(ServletContextEvent e) {} public void sessionCreated(HttpSessionEvent e) { log.info("Session created; session id = " + e.getSession().getId()); } public void sessionDestroyed(HttpSessionEvent e) { log.info("Session destroyed; session id = " + e.getSession().getId()); try { MBeanServer server=MBeanServerLocator.locateJBoss(); TreeCacheMBean cache; ObjectName objectName = new ObjectName(CACHE_OBJECT_NAME); cache=(TreeCacheMBean)MBeanServerInvocationHandler.newProxyInstance(server, objectName, TreeCacheMBean.class, false); Set<String> children = cache.getChildrenNames(FQN); if(children != null) { for(String child : children) { Node node = cache.get(FQN + child); for(Object sessionid : node.getDataKeys()) { log.info("Checking " + sessionid + " for removal."); if(sessionid.equals(e.getSession().getId())) { log.info("removing " + sessionid + " from cache"); cache.remove(FQN + child, sessionid); return; } } } } } catch (Exception ex) { ex.printStackTrace(); } } private void logNumberOfSessions(HttpSessionEvent e) {} }
When copying this code to your own project make sure to change the webapp name to match your webapp. It is currently not keyed to an init parameter or set on startup so it will requite manual editing.
To have this Listener registered you must add it to the web.xml. The following listeners should looks like:
<listener> <listener-class>org.jboss.seam.servlet.SeamListener</listener-class> </listener> <listener> <listener-class>org.domain.example.main.SessionCollectorListener</listener-class> </listener>
2) Admin navigates to session view page and expires the session
sessionManager.xhtml
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:s="http://jboss.com/products/seam/taglib" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:rich="http://richfaces.org/rich" xmlns:a="http://richfaces.org/a4j" template="layout/template.xhtml"> <ui:define name="body"> <h:messages globalOnly="true" styleClass="message"/> <rich:panel> <f:facet name="header">Session Manager</f:facet> <h:form> <rich:dataTable onRowMouseOver="this.style.backgroundColor='#F1F1F1'" onRowMouseOut="this.style.backgroundColor='#{a4jSkin.tableBackgroundColor}'" cellpadding="0" cellspacing="0" width="700" border="0" var="sessionUserPojo" value="#{sessionManager.getCustomSessions()}"> <h:column> <f:facet name="header">FQN</f:facet> #{sessionUserPojo.FQN} </h:column> <h:column> <f:facet name="header">sessionid</f:facet> #{sessionUserPojo.sessionid} </h:column> <h:column> <f:facet name="header">username</f:facet> #{sessionUserPojo.username} </h:column> <h:column> <f:facet name="header">Actions</f:facet> <h:commandButton action="#{sessionManager.removeFromCache(sessionUserPojo)}" value="Remove"/> </h:column> </rich:dataTable> </h:form> </rich:panel> </ui:define> </ui:composition>
In the xhtml code the rich:dataTable iterates over a list of sessions retrieved from the custom FQN in the TomcatClusteringCache we inserted the sessionid and username into from the Authenticator. It presents those list of FQN, sessionid, usernames with a remove button for expiring the session.
Let's take a look at the SessionManager.java and SessionUserPojo.java
SessionUserPojo.java:
public class SessionUserPojo { public SessionUserPojo() {} public SessionUserPojo(String FQN, String sessionid, String username) { this.FQN = FQN; this.sessionid = sessionid; this.username = username; } private String FQN; public String getFQN() { return FQN; } public void setFQN(String fqn) { FQN = fqn; } private String sessionid; public String getSessionid() { return sessionid; } public void setSessionid(String sessionid) { this.sessionid = sessionid; } private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String toString() { return "SessionUserPojo: FQN: " + FQN + " sessionid: " + sessionid + " username: " + username; } }
This is a simple pojo that contains the necessary code for storing the FQN, sessionid, and username to be easily displayed in a dataTable.
SessionManager.java:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.domain.example.main.SessionCollectorListener; import org.jboss.cache.Node; import org.jboss.cache.TreeCacheMBean; import org.jboss.seam.annotations.Logger; import org.jboss.seam.annotations.Name; import org.jboss.seam.log.Log; @Name("sessionManager") public class SessionManager { @Logger private Log log; public List<SessionUserPojo> getCustomSessions() { List<SessionUserPojo> sessionidsUsernames = new ArrayList<SessionUserPojo>(); try { TreeCacheMBean cache = SessionCollectorListener.getTreeCacheMBean(); Set<String> children = cache.getChildrenNames(SessionCollectorListener.FQN); log.info("SessionManager:getCustomSessions children: " + children); if (children != null) { for(String child : children) { Node node = cache.get(SessionCollectorListener.FQN + child); //Add all of the data to the List for (Iterator it=node.getData().entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry)it.next(); String sessionid = (String)entry.getKey(); String username = (String)entry.getValue(); sessionidsUsernames.add(new SessionUserPojo(SessionCollectorListener.FQN + child, sessionid, username)); } } } } catch (Exception e) { e.printStackTrace(); } return sessionidsUsernames; } public List<SessionUserPojo> getRealSessions() { List<SessionUserPojo> sessionidsUsernames = new ArrayList<SessionUserPojo>(); try { TreeCacheMBean cache = SessionCollectorListener.getTreeCacheMBean(); Set<String> children = cache.getChildrenNames(SessionCollectorListener.TREECACHE_FQN); if(children != null) { for(String child : children) { Node node = cache.get(SessionCollectorListener.TREECACHE_FQN + "/" + child); for (Iterator it=node.getData().entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry)it.next(); String key = (String)entry.getKey(); if(!"VERSION".equals(key)) { sessionidsUsernames.add(new SessionUserPojo(SessionCollectorListener.TREECACHE_FQN + "/" + child, key, null)); } } } } } catch (Exception e) { e.printStackTrace(); } return sessionidsUsernames; } /* * Remove the session from the main TreeCache not our generated one as when the * SessionCollectorListener detects the removal, it will remove from the custom Cache node */ public void removeFromCache(SessionUserPojo sessionUserPojo) { log.info("expiring: " + sessionUserPojo.getSessionid()); SessionCollectorListener.getJBossCacheManager().expireSession(sessionUserPojo.getSessionid()); } }
The SessionManager Component handles retrieving both the custom cache objects inserted into the cache and also the real sessionids. I am not displaying the real sessions, but it can just as easily be added to the xhtml view. The most important method here is the removeFromCache method. It is necessary to realize that when you click remove beside a sessionid, the real session is expired directly in the cache, then the custom cache added with the sessionid and username is then removed in the SessionCollectorListener.
3) Session is expired through the CacheManager MBean
In following with the above code, when clicking on Remove on a sessiond id, one will see the following in the console log:
16:45:06,405 INFO [SessionManager] expiring: dHruNWmLQrzSFGidAeKxOQ**.node1 16:45:06,405 INFO [SessionCollectorListener] Session destroyed; session id = dHruNWmLQrzSFGidAeKxOQ**.node1 16:45:06,406 INFO [SessionCollectorListener] Checking dHruNWmLQrzSFGidAeKxOQ**.node1 for removal. 16:45:06,406 INFO [SessionCollectorListener] removing dHruNWmLQrzSFGidAeKxOQ**.node1 from cache
The real session is expired in the SessionManager, then the SessionCollectorListener picks that up and automatically removes the custom data from the cache also.
You will also receive an exception java.lang.IllegalStateException: Please end the HttpSession via org.jboss.seam.web.Session.instance().invalidate() ignore that as we can not get a handle on the specific Seam instance for each node to call invalidate on, so the only option is to expire the session through the CacheManager.
4) The Session Listener picks up the expired session and removes the sessionid and username in the custom FQN in the TomcatClusteringCache
The SessionCollectorListener has already been shown above so let's review the method that expires the session:
public void sessionDestroyed(HttpSessionEvent e) { log.info("Session destroyed; session id = " + e.getSession().getId()); try { MBeanServer server=MBeanServerLocator.locateJBoss(); TreeCacheMBean cache; ObjectName objectName = new ObjectName(CACHE_OBJECT_NAME); cache=(TreeCacheMBean)MBeanServerInvocationHandler.newProxyInstance(server, objectName, TreeCacheMBean.class, false); Set<String> children = cache.getChildrenNames(FQN); if(children != null) { for(String child : children) { Node node = cache.get(FQN + child); for(Object sessionid : node.getDataKeys()) { log.info("Checking " + sessionid + " for removal."); if(sessionid.equals(e.getSession().getId())) { log.info("removing " + sessionid + " from cache"); cache.remove(FQN + child, sessionid); return; } } } } } catch (Exception ex) { ex.printStackTrace(); } }
This method is automatically called by the container whenever a session is destroyed as SessionCollectorListener implements ServletContextListener and HttpSessionListener. When this method is called the sessionid is extracted then the custom FQN is iterated over to look for this specific sessionid, when it is found, it is removed from the cache.
Here is a before and after from removing 2 sessionids from the custom cache:
/usersessionmap /example /71 XoisNwx570QxYG9SBLAEVg**.node1: admin /43 dHruNWmLQrzSFGidAeKxOQ**.node1: admin
After removal:
/usersessionmap /example /71 /43
This is a very basic implementation of this functionality and it could definitely be expanded to provide more features and more data from the ClusterManager.