Please see blog entry: http://www.mojavelinux.com/blog/archives/2006/09/improved_session_tracking/ for explanation.

import java.io.IOException;
import java.util.Date;

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.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>Tracks the user agent and validates the session originator.</p>
 *
 * <p>The {@link HttpSession} is validated by comparing a token that is placed in the session upon creation against
 * a permanent cookie that acts as a unique user-agent (browser) identifier.  If the token does not match, then
 * it means that a URL containing the session id has migrated from one user-agent to another, which is not permitted
 * for security reasons.  In that case, an error is displayed notifying the user that this operation is
 * not allowed and requiring the user to login again.</p>
 * <p>If the session is new, then the user-agent is assigned an identifier (if it hasn't already been assigned one) and
 * stores that identifier as a token in the session, marking this user-agent as the session originator.</p>
 *
 * @web.filter
 *   name="SessionTrackingFilter"
 *   description="Tracks the user agent and validates the session originator"
 * @web.filter-mapping
 *   url-pattern="*.jsf"
 *   dispatcher="REQUEST"
 *
 * @author Dan Allen <dallen@coderyte.com>
 */
public class SessionTrackingFilter implements Filter {

    private static Log logger = LogFactory.getLog( SessionTrackingFilter.class );

    private static final String FILTER_APPLIED = "__session_tracking_filter_applied";

    private static final String USER_AGENT_ID_COOKIE_NAME = "UAID";

    private static final String SESSION_ORIGINATOR_ATTR = "__session_originator";

    public void init( FilterConfig config ) throws ServletException {
    }

    public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain ) throws IOException,
        ServletException {

        if ( servletRequest.getAttribute( FILTER_APPLIED ) != null ) {
            chain.doFilter( servletRequest, servletResponse );
            return;
        }

        if ( !(servletRequest instanceof HttpServletRequest ) || !(servletResponse instanceof HttpServletResponse) ) {
            chain.doFilter( servletRequest, servletResponse );
            return;
        }

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if ( request.getAttribute( FILTER_APPLIED ) == null ) {

            // see if the browser uid is the same as the session originator; if not, deny access
            if ( request.getSession( false ) != null && !request.getSession().isNew() && !isSessionOriginator( request ) ) {
                if ( logger.isDebugEnabled() ) {
                    logger.debug( "Preventing access to session since user agent is not the originator" );
                }

                // QUESTION: should recontructing the url be done in a utility instead?
                StringBuffer url = request.getRequestURL();

                // strip off the session id part if it came with requestURL (jetty does this)
                if (url.indexOf( ";jsessionid=" ) != -1) {
                    url.delete( url.indexOf( ";jsessionid=" ), url.length() );
                }

                if ( !StringUtils.isEmpty( request.getQueryString() ) ) {
                    url.append( "?" );
                    url.append( request.getQueryString() );
                }

                response.sendRedirect( url.toString() );

                // QUESTION: should we instead send either 203 (bad meta), 400 (bad request) or 412 (precondition)
                //response.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "The URL you requested is attempting to use a session that was originated by another computer (or browser).  You will be required to login again." );
                return;
            }

            // create a new session if one does not exist or if it is still unclaimed
            if ( request.getSession( true ).isNew() || request.getSession().getAttribute( SESSION_ORIGINATOR_ATTR ) == null ) {
                // take ownership over this session
                bindToSession( request, response );
            }

            request.setAttribute( FILTER_APPLIED, Boolean.TRUE );
        }

        chain.doFilter( servletRequest, servletResponse );
    }

    private boolean isSessionOriginator( HttpServletRequest request ) {
        // check if session is still unclaimed (created somewhere on the way to this filter)
        if ( request.getSession().getAttribute( SESSION_ORIGINATOR_ATTR ) == null ) {
            return true;
        }

        String userAgentId = getUserAgentId( request );
        return ( userAgentId != null && userAgentId.equals( request.getSession().getAttribute( SESSION_ORIGINATOR_ATTR ) ) );
    }

    private void bindToSession( HttpServletRequest request, HttpServletResponse response ) {
        String userAgentId = getUserAgentId( request );
        if ( StringUtils.isEmpty( userAgentId ) ) {
            userAgentId = generateUserAgentId( request );
            Cookie c = new Cookie( USER_AGENT_ID_COOKIE_NAME, userAgentId );
            // 5 years...should be plenty
            c.setMaxAge( 60 * 60 * 24 * 365 * 5 );
            c.setPath( "/" );
            c.setSecure( request.isSecure() );
            response.addCookie( c );
        }

        request.getSession().setAttribute( SESSION_ORIGINATOR_ATTR, userAgentId );
    }

    private String getUserAgentId( HttpServletRequest request ) {
        if ( request.getCookies() == null ) {
            return null;
        }

        for ( Cookie cookie : request.getCookies() ) {
            if ( cookie.getName().equals( USER_AGENT_ID_COOKIE_NAME ) ) {
                return cookie.getValue();
            }
        }

        return null;
    }

    /**
     * Using the message digest algorithm, generate a unique identifier for this browser,
     * using the server domain name, a random number and the current time in milliseconds
     * as the salts.
     */
    private String generateUserAgentId( HttpServletRequest request ) {
        StringBuffer userAgentId = new StringBuffer();
        userAgentId.append( DigestUtils.md5Hex( request.getRemoteAddr() ) );
        userAgentId.append( "." );
        userAgentId.append( DigestUtils.md5Hex( String.valueOf( Math.round( Math.random() * 2147483647 ) ) ) );
        userAgentId.append( "." );
        userAgentId.append( DigestUtils.md5Hex( String.valueOf( Math.round( new Date().getTime() / 1000 ) ) ) );
        return userAgentId.toString();
    }

    public void destroy() {
    }

}