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() {
}
}