Tuesday, March 20, 2007

User session filter

Capture the User

By default a HTTP session will expire after 30 minutes of inactivity. Although you can change this by the following entry in the web.xml, where the timeout can be set in minutes:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

In most of the web form based authentication solutions it is designed so that when an user logs in, it's User object will be looked up from the database and put as an attribute in the HTTP session using HttpSession#setAttribute(). When the session expires or has been invalidated (when the user closes and reopens the browser window, for example), then the User object will be garbaged and become unavailable. So the user have to login everytime when he visits the website using the same PC after a relatively short period of inactivity or when he opens a new browser session. This can be very annoying after times if the user visits the website (or forum) very frequently.

A good practice is to put an unique session ID in a long-living cookie, and use this session ID to lookup the usersession and the eventual logged in user in the database. This should be totally independent of the lifetime of the HTTP session, so that the user can decide himself when to login and logout. This can be done easily using a Filter.

Back to top

Prepare DTO's

First prepare the DTO's (Data Transfer Objects) for UserSession and User which can be used to hold information about the usersession and the user. You can map those DTO's to the database.

package mydata;

import java.util.Date;

public class UserSession {

    // Init ---------------------------------------------------------------------------------------

    private String sessionId;
    private User user;
    private Date creationDate;
    private Date lastVisit;
    private int hits;

    // Constructors -------------------------------------------------------------------------------

    /**
     * Default constructor. 
     */
    public UserSession() {
        // Keep it alive.
    }
    
    /**
     * Construct new usersession with session ID. 
     */
    public UserSession(String sessionId) {
        this.sessionId = sessionId;
        this.creationDate = new Date();
        this.lastVisit = new Date();
    }
    
    // Implement default getters and setters here.

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Add hit (pageview) to the UserSession. Not necessary, but nice for stats.
     */
    public void addHit() {
        this.hits++;
        this.lastVisit = new Date();
    }

}
package mydata;

public class User {

    // Init ---------------------------------------------------------------------------------------

    private Long id;
    private String username;
    private String password;
    // Implement other properties here, depending on the requirements.
    // For example: email address, firstname, lastname, homepage, etc.

    // Implement default getters and setters here.

}
Back to top

UserSessionFilter

Here is how a UserSessionFilter should look like.

Note: StringUtil#trim() is described here, MathUtil#uniqueID() is described here and HttpServletUtil can be found here.

package mypackage;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import net.balusc.util.HttpServletUtil;
import net.balusc.util.MathUtil;
import net.balusc.util.StringUtil;

import mydao.*;
import mydata.UserSession;
import mydata.User;

/**
 * The UserSession filter.
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/03/user-session-filter.html
 */
public class UserSessionFilter implements Filter {

    // Init ---------------------------------------------------------------------------------------

    private static final String COOKIE_ID = "UserSessionFilterCookieID";
    private static final int COOKIE_MAX_AGE = 31536000; // 60*60*24*365 seconds; 1 year.

    /** The unique ID to set and get the UserSession from the HttpSession. */
    public static final String USERSESSION_ID = "UserSessionFilter.userSession";

    // Actions ------------------------------------------------------------------------------------

    /**
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig filterConfig) {
        // Nothing to do here.
    }

    /**
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, 
     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException
    {
        // Check PathInfo.
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String pathInfo = 
            StringUtil.trim(httpRequest.getRequestURI(), httpRequest.getContextPath());
        if (pathInfo.startsWith("inc")) {
            // This is not necessary, but it might be useful if you want to skip include
            // files for example. You can put include files (subviews, images, css, js)
            // in one folder, called "/inc". If those include files are loaded, then
            // continue the filter chain and abort this filter, because it is usually not
            // necessary to lookup for any UserSession then. Or, if the url-pattern in the
            // web.xml is specific enough, then this if-block can just be removed.
            chain.doFilter(request, response);
            return;
        }

        // Get UserSession from HttpSession.
        HttpSession httpSession = httpRequest.getSession();
        UserSession userSession = (UserSession) httpSession.getAttribute(USERSESSION_ID);

        if (userSession == null) {

            // No UserSession found in HttpSession; lookup SessionId in cookie.
            String sessionId = HttpServletUtil.getCookieValue(httpRequest, COOKIE_ID);

            if (sessionId != null) {

                // SessionId found in cookie. Lookup UserSession by SessionId in database.
                // Do your "SELECT * FROM UserSession WHERE SessionID" thing.
                userSession = new UserSession();
                userSession.setSessionId(sessionId);
                LoadQuery<UserSession> loadQuery = new LoadQuery<UserSession>(userSession);
                try {
                    Dao.execute(loadQuery);
                    userSession = loadQuery.getOne(); 
                    // This can be null. If this is null, then the session is deleted
                    // from DB meanwhile or the cookie is just fake (hackers!).
                } catch (DaoException e) {
                    // Do your exception handling thing.
                    setError("Loading UserSession failed.", e);
                }
            }

            if (userSession == null) {

                // No SessionId found in cookie, or no UserSession found in DB.
                // Create new UserSession.
                // Do your "INSERT INTO UserSession VALUES values" thing.
                sessionId = MathUtil.uniqueID();
                userSession = new UserSession(sessionId);
                try {
                    Dao.execute(new SaveQuery<UserSession>(userSession));
                } catch (DaoException e) {
                    // Do your exception handling thing.
                    setError("Creating UserSession failed.", e);
                }

                // Put SessionId in cookie.
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                HttpServletUtil.setCookieValue(
                    httpResponse, COOKIE_ID, sessionId, COOKIE_MAX_AGE);
            }

            // Set UserSession in current HttpSession.
            httpSession.setAttribute(USERSESSION_ID, userSession);
        }

        // Add hit and update UserSession.
        // Do your "UPDATE UserSession SET values WHERE SessionID" thing.
        userSession.addHit();
        SaveQuery<UserSession> saveQuery = new SaveQuery<UserSession>(userSession);
        try {
            Dao.execute(saveQuery);
        } catch (DaoException e) {
            // Do your exception handling thing.
            setError("Updating UserSession failed.", e);
        }
        if (saveQuery.getAffectedRows() == 0) { 
            // UserSession is deleted from DB meanwhile.
            // Reset current UserSession and re-filter.
            httpSession.setAttribute(USERSESSION_ID, null);
            doFilter(request, response, chain);
        } else {
            // Continue filtering.
            chain.doFilter(request, response);
        }
    }

    /**
     * @see javax.servlet.Filter#destroy()
     */
    public void destroy() {
        // Apparently there's nothing to destroy?
    }

}

You can activate the UserSessionFilter by adding the following lines to the web.xml. Take note: the filters are executed in the order as they are definied in the web.xml.

<filter>
    <filter-name>UserSessionFilter</filter-name>
    <filter-class>mypackage.UserSessionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>UserSessionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
Back to top

Login and Logout

Now, with such an UserSessionFilter the user can decide himself when to login and logout. Here is an example how you can let the user login and logout. It is just all about putting and removing the User object from the UserSession object.

The basic JSF code for the login:

<h:form>
    <h:panelGrid columns="2">
        <h:outputText value="Username" />
        <h:inputText value="#{myBean.username}" />

        <h:outputText value="Password" />
        <h:inputSecret value="#{myBean.password}" />

        <h:panelGroup />
        <h:commandButton value="login" action="#{myBean.login}" />
    </h:panelGrid>
</h:form>

And the basic JSF code for the logout:

<h:form>
    <h:commandButton value="logout" action="#{myBean.logout}" />
</h:form>

Finally the backing bean's code. Note: MathUtil#hashMD5() is described here.

package mypackage;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;

import net.balusc.util.MathUtil;

import mydao.*;
import mydata.UserSession;
import mydata.User;

public class MyBean {

    // Init --------------------------------------------------------------------------------------

    private String username;
    private String password;
    // With getters and setters.

    // Actions -----------------------------------------------------------------------------------

    public void login() {

        // Do your "SELECT * FROM User WHERE username AND password" thing.
        User user = new User();
        user.setUsername(username);
        user.setPassword(MathUtil.hashMD5(password));
        LoadQuery<User> loadQuery = new LoadQuery<User>(user);
        try {
            Dao.execute(loadQuery);
            user = loadQuery.getOne(); 
        } catch (DaoException e) {
            // Do your exception handling thing.
            setError("Loading User failed.", e);
        }

        if (user != null) {

            // User found. Put the User in the UserSession.
            UserSesison userSession = getUserSession();
            userSession.setUser(user);

            // Do your "UPDATE UserSession SET values WHERE SessionID" thing.
            try {
                Dao.execute(new SaveQuery<UserSession>(userSession));
            } catch (DaoException e) {
                // Do your exception handling thing.
                setError("Updating UserSession failed.", e);
            }
        } else {
            // Do your error handling thing.
            setError("Unknown username and/or invalid password.");
        }
    }

    public void logout() {

        // Just null out the user.
        UserSesison userSession = getUserSession();
        userSession.setUser(null);

        // Do your "UPDATE UserSession SET values WHERE SessionID" thing.
        try {
            Dao.execute(new SaveQuery<UserSession>(userSession));
        } catch (DaoException e) {
            // Do your exception handling thing.
            setError("Updating UserSession failed.", e);
        }
    }

    // Getters -----------------------------------------------------------------------------------

    public UserSession getUserSession() {
        return (UserSession) FacesContext.getCurrentInstance().getExternalContext()
            .getSessionMap().get(UserSessionFilter.USERSESSION_ID);
    }

    // Checkers ----------------------------------------------------------------------------------

    public boolean isUserLoggedin() {
        return getUserSession().getUser() != null;
    }

}

The isUserLoggedin() method can be very useful for JSF pages. You can use it in the rendered attribute of any component for example.

<h:panelGroup rendered="#{!myBean.userLoggedin}">
    <h:outputText value="You are not logged in." />
    <%-- put the login code here --%>
</h:panelGroup>

<h:panelGroup rendered="#{myBean.userLoggedin}">
    <h:outputText value="You are logged in as #{myBean.userSession.user.username}." />
    <%-- put the logout code here --%>
</h:panelGroup>
Back to top

Security considerations

Be aware that the session ID ought to be unique for every usersession. In the above example the session ID is a 32-char hexadecimal string obtained from MathUtil#uniqueID() which is built using the thread ID, the time in millis and two randomly generated parts. Although this is much harder to hack or wild-guess than passwords, it might be a good practice to retrieve the IP address from the user using HttpServletRequest#getRemoteAddr() and put it along the session ID in the database (thus not in the cookie!). When looking up the usersession in the database you can use SELECT * FROM UserSession WHERE SessionID AND RemoteAddr. One big con is that this does not work very well when the user has a dynamic IP address. But it would be nice if such an option will be provided in the login page: "Lock this usersession to the current IP address" or so.

Last note: the "official" container managed session ID as is in HttpSession#getId() is not alltime-unique. It is only unique inside the running webcontainer between all non-expired HttpSessions. So don't even consider to use it as session ID for in the cookie.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) March 2007, BalusC

7 comments:

cat3dog4 said...

Hi BalusC,

it's very helpful. a question: can I write a method boolean isAffected() to compare the userSession before and after saving, instead of saveQuery.getAffectedRows().
and there is a method in ResultSet "boolean rowUpdated()" can i use this method to check if the data is updated in the database?

thank you for help.

BalusC said...

You can do that. Just do your DAO thing :) The classnames and methodnames of my DAO example should be descriptive enough to get a clear picture.

dim5b said...

Hi, quick uestion-clarification.
For a login application one would Require 2 managed-beans??? One request bean to support the login form in the login page. And a session scoped bean that stores throughout the user session his u/p details for re-login is this correct??? when the user log-off the session bean is removed from the session (session.invalidate())
Is this a valid approach maybe a sort example could be post under this blog???
thanks dim5b

Loi said...

Hi,
I'm new to JSF and I really need your help. I saw you use import mydao.* package. I was wondering, how do I import that, is that a jar file and Hibernate or something? Thanks

BalusC said...

Just do your DAO thing. The classnames and methodnames are descriptive enough to get a picture what they does.

sidra said...

Hi, please tell me how can I solve my errors concerning LoadQuery<>, SaveQuery<>, Dao, DaoException..?

BalusC said...

Just do your DAO thing. The classnames and methodnames are descriptive enough to get a picture what they does.