Java

Tracking User Sessions Made Simple(r)

Knowing which users are logged into your applications, what they’re accessing, and when they might leave can be invaluable. In our environment, we have multiple applications fronted by a thin, portal-like front end. For the purposes of the example, we want to know who’s using which webapp, and when they logged in. We’ll also track the session timeout, just for kicks.

The bulk of the problem is finding a common place to store the information, and accessing and structuring that data. In this case, we have a shared application configured for the root context (e.g. http://localhost/), but you can really choose any app you have deployed. The core of this feature is the Http Servlet event model, particularly the javax.servlet.http.HttpSessionBindingListener, which fires an event when it is attached to a session (in our case, at login), and when it is detached from the session (at session invalidation, unless removed/replaced by other code). An example listener follows:

public class PortalSessionBindingListener implements HttpSessionBindingListener
{
   private org.apache.commons.logging.Log log =
   org.apache.commons.logging.LogFactory.getLog(this.getClass());
  
   private String username;
   private String appname;
  
   public PortalSessionBindingListener( String username, String appname )
   {
     this.username = username;
     this.appname = appname;
   }

   public void valueBound(HttpSessionBindingEvent event)
   {
     String sessionId = event.getSession().getId();
     String sessionTimeout = Integer.toString(event.getSession().getMaxInactiveInterval());
     String timeLoggedIn = (new Date()).toString();
     // ServletContext of shared app
     ServletContext sharedContext = event.getSession().getServletContext().getContext(“/”);
    
     Map userMap = (Map) sharedContext.getAttribute(“userMap”);
     if (userMap == null)
     {
       userMap = new HashMap();
       sharedContext.setAttribute(“userMap”, userMap);
     }
     //Get active sessions for this user
     Map userSessionMap = (Map) userMap.get(username);
     if (userSessionMap == null)
     {
       userSessionMap = new HashMap();
       userMap.put(username, userSessionMap);
     }
     SessionListEntry entry = (SessionListEntry) userSessionMap.get(sessionId);
     if ( entry == null)
     {
       entry = new SessionListEntry(username, appname, timeLoggedIn,
           sessionTimeout, sessionId);
       userSessionMap.put(sessionId, entry);
     }
   }
  
   public void valueUnbound(HttpSessionBindingEvent event)
   {
     String sessionId = event.getSession().getId();
     // ServletContext of shared app
     ServletContext sharedContext = event.getSession().getServletContext().getContext(“/”);
    
     Map userMap = (Map) sharedContext.getAttribute(“userMap”);
     Map userSessionMap = (Map) userMap.get(username);
     if (userSessionMap != null)
     {
       SessionListEntry entry = (SessionListEntry) userSessionMap.get(sessionId);
       if (entry != null)
       {
         userSessionMap.remove(sessionId);
         if (userSessionMap.isEmpty())
         {
           userMap.remove(userSessionMap);
         }
       }
     }
   }
}

SessionListEntry is just a simple JavaBean style object (except with no setters) that can easily be extended to hold any info you might need. You’ll probably want to wrap the whole userMap/UserSessionMap in some objects so it’s cleaner and more OO, but it’s easier to illustrate this way. We’re using struts in all of our apps, so we have defined a struts action, SessionTrackerAction, with the following execute method:

   public ActionForward execute(
     ActionMapping mapping,
     ActionForm form,
     HttpServletRequest request,
     HttpServletResponse response)
     throws Exception
   {
     //Appname must be configured in parameter
     String appname = mapping.getParameter();
     String username = request.getUserPrincipal().getName();
     PortalSessionBindingListener listener = new PortalSessionBindingListener( username, appname);
     request.getSession().setAttribute(“portalListener”, listener);
     return mapping.findForward(“success”);
   }

If you have control over the links users use to access the application (as we do), you can route all users through a single link for each application. For each application, we use an index.do as the entry point to the app, and we define that action as:

   <action path=”/index”
       type=”com.acme.portal.action.SessionTrackerAction”
       parameter=”appName”>
     <forward name=”success” path=”.index”/> <!– forwarded to tiles template –>
   </action>

If you don’t have this level of control, you could easily put a servlet filter on the application that executes the code we’ve embedded in the struts action when session.isNew() [== true] and request.getUserPrincipal() != null. Another application of a servlet filter could be to update the SessionListEntry each time the user requests a page, in order to precisely know when their session is due to expire. While I question whether that’s worth the overhead, it IS an interesting application of the code.

Advertisements