Monday, August 13, 2007

A user-configurable timeout for your Wicket application

I had a need to make the session timeout in my app configurable by the end-user. Also, I needed to make sure the login page never expires and that the user never sees the Wicket "session expired" page. In Wicket 1.2.6, there are no stateless pages, especially if the page has a form on it. There is a workaround posted by Igor using an Ajax timer. I used this as the basis of my timeout behavior.


First I needed to make my default session timeout to a larger value than anything the user could choose. This is done in web.xml:


web.xml


<session-config>
<session-timeout>4096</session-timeout>
</session-config>

Next I needed to provide a way for the user to change the timeout in a preferences page. This was done by providing a field where the user could type in a value in minutes, which is then serialized to the database.


Next I made the login page non-expiring. The following code is added to the end of the constructor of the login page.



  • Due to a bug in Wicket 1.2.6 you have to attach the timer to a component (that is, you can't just add it to the page). I arbitrarily attach it to the form.

  • The timer interval is how often the timer fires and pings the server. This value (TIMERINTERVALMINUTES) is specified in the base page for all my web pages EXCEPT the login page. My login page doesn't subclass the base page because I never want it to expire, whereas I do want all my other pages to expire.

  • Note that we don't do anything in the onTimer method.


MyLogin.java


form.add(new AbstractAjaxTimerBehavior(Duration.minutes(MyBasePage.TIMERINTERVALMINUTES))
{
protected void onTimer(AjaxRequestTarget target)
{
}
});

Next I need to add a field to the session that remembers the remaining timeout value. This value gets decremented every TIMERINTERVALMINUTES period, until the page expires, after which it is reset to the starting value. It also gets reset whenever a new page is displayed (i.e. whenever the user takes an action).


MySession.java


public final class MySession extends WebSession
{
/**
* Keeps track of session timeout. Negative values are special
* because they indicates the session has expired.
*/
private int minutesToTimeout = -1;
...
public int getMinutesToTimeout()
{
return minutesToTimeout;
}
public void setMinutesToTimeout(int minutesToTimeout)
{
this.minutesToTimeout = minutesToTimeout;
}

Now comes the base page for all my web pages.



  • It is abstract because it is not meant to be instantiated as is.

  • The timer interval defaults to 5 minutes.

  • I add a logout link that when clicked, invalidates the session and goes back to the login page.

  • I add the Ajax timer behavior to the logout link. As mentioned above, due to a bug in Wicket 1.2.6 you have to attach the timer to a component, in this case the logout link.

  • If the page is not visible, we return from onTimer immediately. This is important because otherwise, every page in the PageMap will decrement the counter. We only want to do so for the currently visible page.

  • We decrement the remaining timeout value in the session.

  • If the session has timed out (remaining timeout value is less than zero), we invalidate the session and return to the login page.

  • We override onBeforeRender() so that every time the user takes an action, we reset the timer.


MyBasePage.java


public abstract class MyBasePage extends WebPage
{
/** We decrement the logout timer every 5 minutes */
public static final int TIMERINTERVALMINUTES = 5;
...
public MyBasePage()
{
...
Link logoutLink = new Link("logoutLink")
{
public void onClick()
{
session.invalidate();
setResponsePage(MyLogin.class);
}
};
add(logoutLink);
...
logoutLink.add(new AbstractAjaxTimerBehavior(Duration.minutes(TIMERINTERVALMINUTES))
{
protected void onTimer(AjaxRequestTarget target)
{
if (!MyBasePage.this.isVisible())
{
// If we don't check this, it will decrement for every page
// in the cache.
return;
}
session.setMinutesToTimeout(session.getMinutesToTimeout()-TIMERINTERVALMINUTES);
if (session.getMinutesToTimeout() < 0)
{
session.invalidate();
setResponsePage(MyLogin.class);
}
}
});
...
}
...
/**
* We override this method so that every time
* the user takes an action, we reset the timer.
*/
protected void onBeforeRender()
{
super.onBeforeRender();
/**
* Get the logout expiration timer value (specified in the database). We
* retrieve it from the DB each time through this method because the user
* might set it in the prefs page, so it could change at any time.
*/
int uiTimeoutValue = this.getMyApplication.getTimeoutValue());
this.getMySession().setMinutesToTimeout(uiTimeoutValue);
}

The perceptive reader would notice that onBeforeRender() will not be called if only a portion of the page is rendered (for example, if Ajax is used to update just the parts of the page that have changed). In this case you could create a reset method in the base page that gets called from any such Ajax action.

3 comments:

Anthony said...

How could one adapt this in order for the timeout counter to be reset upon ajax requests like automatic form validation?

Julian Sinai said...

Anthony, from your ajax handler you could call the same code that's in onBeforeRender() to reset the timer: getMySession().setMinutesToTimeout(uiTimeoutValue);

Note that I found a better way to support user-configurable timeouts: override HttpSessionStore, create this method:

public void setMaxInactiveInterval(Request request, int mins)
{
// Set the session timeout to match the user preference
HttpSession httpSession = this.getHttpSession(this.toWebRequest(request));
httpSession.setMaxInactiveInterval(60 * mins);
}

You can get access to the session store anywhere in the application from the session using Session.get().getSessionStore(), then call the method above.

Roger said...

I guess you'd also have to check the page map and only decrement the counter if its null, otherwise you'll decrement the counter multiple times if you have multiple tabs open on the same app...