I\'ve got an existing Grails Web application that is in production and has a 30 minute session timeout. We are running Tomcat (tcServer).
When a user is authentic
I'd go with a Grails filter that does something similar to what The-MeLLeR is proposing without the unnecessary loop through all sessions:
class AjaxTimeoutFilters {
int sessionTimeout = 30 * 60 * 1000
private static final String TIMEOUT_KEY = 'TIMEOUT_KEY'
def filters = {
all(controller:'*', action:'*') {
before = {
if (request.xhr) {
Long lastAccess = session[TIMEOUT_KEY]
if (lastAccess == null) {
// TODO
return false
}
if (System.currentTimeMillis() - lastAccess > sessionTimeout) {
session.invalidate()
// TODO - render response to trigger client redirect
return false
}
}
else {
session[TIMEOUT_KEY] = System.currentTimeMillis()
}
true
}
}
}
}
The session timeout should be dependency-injected or otherwise kept in sync with the value in web.xml.
There are two remaining issues. One is the case where there's an Ajax request but no previous non-Ajax request (lastAccess == null). The other is how to redirect the browser to a login page or wherever you need to go when there's an Ajax request after 30 minutes of no non-Ajax activity. You'd have to render JSON or some other response that the client would check to know that it's been timed out and do a client-side redirect.
Nope not possible...
One option is the following:
1) create a javax.servlet.Filter and store the timestamp of the last (non-ajax) pageview on the session.
2) create a javax.servlet.http.HttpSessionListener to store all the active sessions.
3) use a background thread to invalidate all expired sessions.
Sample Code:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class LastAccessFilter implements Filter, HttpSessionListener {
private static final Object SYNC_OBJECT = new Object();
private static final String LAST_ACCESSED = "lastAccessed";
private boolean backgroundThreadEnabled;
public void destroy() {
synchronized (SYNC_OBJECT){
backgroundThreadEnabled = false;
SYNC_OBJECT.notifyAll();
}
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
if (req instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
if(!isAjax(httpServletRequest)){
httpServletRequest.getSession().setAttribute(LAST_ACCESSED, System.currentTimeMillis());
}
}
chain.doFilter(req, resp);
}
public static boolean isAjax(request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
public void init(FilterConfig config) throws ServletException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (LastAccessFilter.this.backgroundThreadEnabled) {
synchronized (SYNC_OBJECT) {
try {
SYNC_OBJECT.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (LastAccessFilter.this.backgroundThreadEnabled) {
HttpSession[] sessions;
synchronized (activeSessions){
sessions = activeSessions.toArray(new HttpSession[activeSessions.size()]);
}
cleanupInactiveSessions(sessions);
}
}
}
}
private void cleanupInactiveSessions(HttpSession[] sessions) {
for (HttpSession session : sessions) {
Object lastAccessedObject = session.getAttribute(LAST_ACCESSED);
if(lastAccessedObject == null) continue;
long lastAccessed = (Long)lastAccessedObject;
if(System.currentTimeMillis() > (lastAccessed + 1800000)){//30 Minutes
session.invalidate();
}
}
}
});
t.setDaemon(true);
this.backgroundThreadEnabled = true;
t.start();
}
private final List<HttpSession> activeSessions = new ArrayList<HttpSession>();
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
synchronized (activeSessions) {
this.activeSessions.add(httpSessionEvent.getSession());
}
}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
synchronized (activeSessions) {
this.activeSessions.remove(httpSessionEvent.getSession());
}
}
}