views:

58

answers:

3

I am developing a Java web application that will run on a secure intranet and does not require a user login. The application does, however, keep conversational state in an HttpSession. User input is not persisted to the database until they explicitly click a save button at some stage in the conversation. Until then, their input is retained in the HttpSession object. If their session expires, the user must be directed to a page that informs them of the session expiry.

This is working fine except for a problem with the redirect. When a user allows their session to sit idle for longer than the time defined in <session-timeout>, the session expires as expected. However, my attempt to redirect the user to a simple "Your session has expired" page seems to have backfired. The redirect works alright, but unless the user closes all the open browser windows on their desktop (not just the ones that were open to my web app page) they will continue being redirected to the "session expired" page forever.

Here are my constraints:

  • Client workstations use Internet Explorer. This is company-wide and will not change anytime soon.
  • Users will have mulitple instances of IE open on their desktop as part of their normal workflow. Telling them to close all instances of IE is not acceptable.
  • Not using any AJAX components in this web app

I've implemented the redirect with a Java Servlet Filter. Here are the relevant code snippets:

@Override
public void doFilter(
        ServletRequest request, 
        ServletResponse response,
        FilterChain filterChain) 
        throws IOException, ServletException {
    Validate.notNull(filterConfig);
    Validate.isTrue(request instanceof HttpServletRequest);
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    String requestedSessionId = httpServletRequest.getRequestedSessionId();
    logger.info("requestedSessionId: " + requestedSessionId);
    HttpSession httpSession = httpServletRequest.getSession(false);

    if (requestedSessionId == null) {
        // No need to do anything here if no session exists yet
        logger.debug("No session exists yet");
        filterChain.doFilter(request, response);
    } else {
        if (httpSession == null) {
            Validate.isTrue(response instanceof HttpServletResponse);
            HttpServletResponse httpServletResponse =
                (HttpServletResponse) response;
            handleSessionExpired(
                httpServletRequest,
                httpServletResponse);
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Session OK | requested URL: " + 
                    httpServletRequest.getRequestURL().toString());
                }
                filterChain.doFilter(request, response);
            }
        }
    }
}

private void handleSessionExpired(
        HttpServletRequest httpServletRequest,
        HttpServletResponse httpServletResponse) 
        throws IOException {
    logger.warn("expired session | id: " + 
        httpServletRequest.getRequestedSessionId());
    String expirationPageURL = 
        httpServletRequest.getContextPath() + "/" + 
        "SessionExpiredNotification.html";
    httpServletResponse.sendRedirect(expirationPageURL);
}

The SessionExpiredNotification.html page is meant to be the end of the line. The user should close this browser window and open a new one if they want to start a new conversation. The problem is that the new browser window still wants to use the old session id value that was associated with the now invalidated session whenever the user has any other instances of Internet Explorer open on their desktop. This isn't specific to IE, as I have confirmed that Firefox behaves exactly the same way.

When this code is reached in my Filter:

String requestedSessionId = httpServletRequest.getRequestedSessionId();
logger.info("requestedSessionId: " + requestedSessionId);

I can see that the client-side browser is still holding on to the old session id value and requesting it over and over again.

I'm not sure if it is relevant, but my web application container is Tomcat 6.x.

MY QUESTION:
How can the server web app signal the client workstation that a session id is no longer valid such that the client will discard it?

A: 

You can make a poll mechanism using AJAX, and maintain a cookie whether your session is alive or died. then from cookie you can restrict client

org.life.java
I'm not using any AJAX on this project. I'm also fairly restricted with respect to adding new libraries.
Jim Tough
+2  A: 

If request.getSession(false) returns null, you should then create a new session. You can do this by calling request.getSession(true).

In other words, at no point in the code posted are you instructing the servlet container to create a new session and assign the current request to it.

matt b
I need the user to see a simple page that tells them the old session has expired and their unsaved input has been lost before they start a new session. If I call getSession(true) then won't it just carry on with a new conversation without any indication that the last one was discarded?
Jim Tough
No, this would create a new session for the user, which the servlet container would respond to by sending an updated JSESSIONID cookie to the user. Your redirect to SessionExpiredNotification.html would still work.
matt b
@matt_b OK, I'll make some changes and try it your way
Jim Tough
A: 

This is the solution I've used. I should not have been calling sendRedirect() in my filter, since that will never return a new JSESSIONID to the client browser. I need to send an actual response that kills the old JSESSIONID Cookie, otherwise the client browser will just keep trying to use it. My first thought was to get the JSESSIONID Cookie from the request header, set it to be expired, then include the expired Cookie in the response so the client will act on the expiry. Other Stackoverflow users suggested that was not a clean solution, so I have scrapped that idea.

I replaced my handleSessionExpired() method in the Filter to use a RequestDispatcher. This will allow my Filter to dispatch the request to a custom "your session is expired" JSP page. Unlike a redirect, the RequestDispatcher will send a proper response to the client.

Here is the primary method in my Filter:

@Override
public void doFilter(
        ServletRequest request, 
        ServletResponse response,
        FilterChain filterChain) 
        throws IOException, ServletException {
    Validate.notNull(filterConfig);
    Validate.isTrue(request instanceof HttpServletRequest);
    HttpServletRequest httpServletRequest =
        (HttpServletRequest) request;
    String requestedSessionId = httpServletRequest.getRequestedSessionId();
    logger.info("requestedSessionId: " + requestedSessionId);
    HttpSession httpSession = httpServletRequest.getSession(false);

    if (requestedSessionId == null) {
        // No need to do anything here if no session exists yet
        logger.debug("No session exists yet");
        filterChain.doFilter(request, response);
    } else {
        if (httpSession == null) {
            Validate.isTrue(response instanceof HttpServletResponse);
            HttpServletResponse httpServletResponse =
                (HttpServletResponse) response;
            handleSessionExpired(
                httpServletRequest,
                httpServletResponse);
        } else {
            filterChain.doFilter(request, response);
        }
    }
}

My handleSessionExpired() method is very simple. This extra method call only exists because of another special use case that my filter needs to handle (but is not relevant to my original question).

private void handleSessionExpired(
        HttpServletRequest httpServletRequest,
        HttpServletResponse httpServletResponse) 
        throws IOException, ServletException {
    logger.info("expired session | id: " + 
        httpServletRequest.getRequestedSessionId());
    sendSessionExpiredResponse(httpServletRequest, httpServletResponse);
}

My sendSessionExpiredResponse() is also quite simple. The call to getSession() will cause a new session to be created (since no valid HttpSession already exists at this point) and the JSESSIONID to be included in the response. That takes care of cleaning the obsolete session id on the client side. I set a request attribute "isExpired" so the session expiry JSP knows to display a message saying that the session is expired. I'm also using the same JSP page when a user manually ends a session, so I use the attribute to decide what text to display on the page.

private void sendSessionExpiredResponse(
        HttpServletRequest httpServletRequest,
        HttpServletResponse httpServletResponse) 
        throws IOException, ServletException {
    httpServletRequest.getSession(true); // force valid session to exist
    httpServletRequest.setAttribute("isExpired", true);
    RequestDispatcher rd = filterConfig.getServletContext()
        .getNamedDispatcher("SessionExpired");
    rd.forward(httpServletRequest, httpServletResponse);
}

The getNamedDispatcher() call gets the JSP via my entry in web.xml:

<servlet>
    <servlet-name>SessionExpired</servlet-name>
    <jsp-file>/SessionExpired.jsp</jsp-file>
</servlet>
Jim Tough