Partiendo de una aplicación en la que es necesario iniciar sesión, mediante un usuario y contraseña. ¿Como podemos controlar que este usuario no tenga iniciada más de una sesión? ¿Como podemos controlar que un usuario no ejecute 2 acciones simultaneas por ejemplo desde 2 pestañas?
Nos planteamos 3 opciones:
1.- No permitir iniciar sesión con un usuario que ya tiene iniciada una sesión anterior.
Esta opción es la más complicada, debido a que hay que controlar cuando el usuario finaliza una sesión para liberarla y debido al magnifico mundo de las pestañas y de los diferentes navegadores.
Tras investigar diversas posibilidades basándonos en:
- Control de pestañas y ventanas de los navegadores:
http://www.codeproject.com/Articles/35859/Detect-and-prevent-multiple-windows-or-tab-usage-i
- Control de Ids de sesiones y usuarios en sesión mediante SessionListener e Interceptors de Struts.
Dimos por invalida esta solución, debido al problema de controlar el cierre de sesión al cerrar una ventana o pestaña en los diferentes navegadores.
Muchos pensareis que con un SessionListener y configurando el timeout de la sesión, se puede dar por finalizada la sesión del usuario al controlar el tiempo de sesión inactiva. El problema que esto conlleva es que el usuario si cierra una ventana no se le permite iniciar sesión ya que la sesión aun no ha expirado y debería esperar el tiempo correspondiente para poder volver a iniciar una sesión.
2.- Nueva ventana auxiliar.
Opción no probada, que pensamos que utilizan algunas aplicaciones.
Crear una nueva ventana tras realizar el login y ejecutar la aplicación en esa nueva ventana. Con este comportamiento y bloqueando diferentes opciones sobre esa ventana se puede llegar a controlar que en cuanto cierren esa ventana se da por finalizada la sesión.
Debido a que no es el comportamiento que queremos para nuestra aplicación. La descartamos.
3.- Permitir el inicio de sesión desde otra ventana o pestaña pero invalidar la sesión anterior.
Configuramos correctamente el web.xml de nuestra aplicación web.
1: <!-- Colocar correctamente en el web.xml según corresponda -->
2:
3: <!-- Tiempo de expiración de la sesión-->
4: <session-config>
5: <session-timeout>30</session-timeout>
6: </session-config>
7:
8: !-- Listener Set atributes Session -->
9: <listener>
10: <description>HttpSessionAttributeListener</description>
11: <listener-class>net.fullcarga.titan6.servlet.SessionAttributeListener</listener-class>
12: </listener>
13:
14: <!-- Listener create/destroy Sessión -->
15: <listener>
16: <description>HttpSessionListener</description>
17: <listener-class>net.fullcarga.titan6.servlet.SessionListener</listener-class>
18: </listener>
Implementamos el SessionListener:
1: package net.ejemplo.sesiones.servlet;
2:
3: import javax.servlet.http.*;
4: import net.ejemplo.sesiones.service.SessionMonitor;
5: import org.apache.commons.logging.Log;
6: import org.apache.commons.logging.LogFactory;
7: import org.springframework.context.ApplicationContext;
8: import org.springframework.web.context.support.WebApplicationContextUtils;
9:
10: /**
11: *
12: * @author terinventer
13: */
14: public class SessionListener implements HttpSessionListener { 15:
16: protected Log log = LogFactory.getLog(SessionListener.class);
17: /**
18: * No se implementa porque no lo necesitamos.
19: * @param se
20: */
21: public void sessionCreated(HttpSessionEvent se) { 22: // Not implenented
23: }
24: /**
25: * Captura el evento, cuando una sesión es finalizada.
26: * @param sessionEvent
27: */
28: public void sessionDestroyed(HttpSessionEvent sessionEvent) { 29: HttpSession session = sessionEvent.getSession();
30: ApplicationContext ctx =
31: WebApplicationContextUtils.getWebApplicationContext(session.getServletContext());
32: SessionMonitor sessionService =
33: (SessionMonitor) ctx.getBean("sessionMonitor"); 34: sessionService.sessionDestroyed(session.getId());
35:
36: }
37: }
Implementamos el SessionAttributeListener:
1: package net.ejemplo.sesiones.servlet;
2:
3: import javax.servlet.http.*;
4: import net.ejemplo.sesiones.EjemploConstants;
5: import net.ejemplo.sesiones.model.usuario.Usuario;
6: import net.ejemplo.sesiones.service.SessionMonitor;
7: import org.apache.commons.logging.Log;
8: import org.apache.commons.logging.LogFactory;
9: import org.springframework.context.ApplicationContext;
10: import org.springframework.web.context.support.WebApplicationContextUtils;
11:
12: /**
13: *
14: * @author terinventer
15: */
16: public class SessionAttributeListener implements HttpSessionAttributeListener { 17:
18: protected Log log = LogFactory.getLog(SessionAttributeListener.class);
19:
20: /**
21: * Comprueba que el nuevo atributo para la sesión corresponde con el
22: * identificador del usuario y actualizamos el HashMap
23: *
24: * @param sessionEvent
25: */
26: public void attributeAdded(HttpSessionBindingEvent sessionEvent) { 27: HttpSession session = sessionEvent.getSession();
28: if (sessionEvent.getName().equals(EjemploConstants.USER_SESSION)) { 29: ApplicationContext ctx =
30: WebApplicationContextUtils.getWebApplicationContext(session.getServletContext());
31: SessionMonitor sessionService =
32: (SessionMonitor) ctx.getBean("sessionMonitor"); 33: sessionService.attributeAdded(session.getId(), sessionEvent.getValue());
34: }
35: }
36:
37: /**
38: * Comprueba que el atributo eliminado para la sesión corresponde con el
39: * identificador del usuario y actualizamos el HashMap
40: *
41: * @param sessionEvent
42: */
43: public void attributeRemoved(HttpSessionBindingEvent sessionEvent) { 44: HttpSession session = sessionEvent.getSession();
45: if (sessionEvent.getName().equals(EjemploConstants.USER_SESSION)) { 46: ApplicationContext ctx =
47: WebApplicationContextUtils.getWebApplicationContext(session.getServletContext());
48:
49: SessionMonitor sessionService =
50: (SessionMonitor) ctx.getBean("sessionMonitor"); 51:
52: sessionService.attributeRemoved(sessionEvent.getValue());
53: }
54: }
55:
56: /**
57: * Comprueba que el atributo actualizado en sesión corresponde con el
58: * identificador del usuario y actualizamos el HashMap
59: *
60: * @param sessionEvent
61: */
62: public void attributeReplaced(HttpSessionBindingEvent sessionEvent) { 63: HttpSession session = sessionEvent.getSession();
64: if (sessionEvent.getName().equals(EjemploConstants.USER_SESSION)) { 65: // Si se obtiene el valor del evento, se obtiene el valor anterior, No nos vale.
66: // Hay que obtener el valor en sesión, porque ya es el nuevo valor.
67: Usuario userSession = ((Usuario) session.getAttribute(EjemploConstants.USER_SESSION));
68: ApplicationContext ctx =
69: WebApplicationContextUtils.getWebApplicationContext(session.getServletContext());
70: SessionMonitor sessionService =
71: (SessionMonitor) ctx.getBean("sessionMonitor"); 72: sessionService.attributeAdded(session.getId(), userSession);
73: }
74: }
75: }
Definimos el bean SessionMonitor en el applicationContext.xml utilizado por Spring.
1: <?xml version="1.0" encoding="UTF-8"?>
2: <beans xmlns="http://www.springframework.org/schema/beans"
3: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4: xmlns:p="http://www.springframework.org/schema/p"
5: xmlns:context="http://www.springframework.org/schema/context"
6: xmlns:tx="http://www.springframework.org/schema/tx"
7: xsi:schemaLocation="
8: http://www.springframework.org/schema/beans
9: http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
10: http://www.springframework.org/schema/tx
11: http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
12: http://www.springframework.org/schema/context
13: http://www.springframework.org/schema/context/spring-context-2.5.xsd">
14: <context:annotation-config/>
15: <!-- Bean Singleton que mantiene el Map con los usuarios e Ids en Session-->
16: <bean id="sessionMonitor" scope="singleton" class="net.ejemplo.sesiones.service.SessionMonitor" />
17:
18: </beans>
Se implementa el bean SessionMonitor, incluye los métodos necesarios para mantener el HashMap de Usuarios en sesión actualizados.
1: package net.ejemplo.sesiones.service;
2:
3: import java.util.concurrent.ConcurrentHashMap;
4: import net.ejemplo.sesiones.model.session.UsuariosSession;
5: import net.ejemplo.sesiones.model.usuario.Usuario;
6: import org.apache.commons.logging.Log;
7: import org.apache.commons.logging.LogFactory;
8: /**
9: *
10: * @author terinventer
11: */
12: public class SessionMonitor { 13:
14: protected final Log logger = LogFactory.getLog(getClass());
15: // Map que mantiene los valores de los usuarios en sesión
16: protected ConcurrentHashMap<String, UsuariosSession> usuariosSession = new ConcurrentHashMap<String, UsuariosSession>();
17:
18: /**
19: * Actualizar el HashMap de usuarios con el nuevo valor
20: *
21: * @param id
22: * @param object
23: */
24: public void attributeAdded(String id, Object object) { 25: Usuario usuario = (Usuario) object;
26: UsuariosSession usuarioSession = new UsuariosSession();
27: usuarioSession.setIdSession(id);
28: usuarioSession.setUsuario(usuario.getUsuario());
29: // Se actualiza el idSession, utilizado por ese usuario.
30: // Invalidando el idSession previo
31: this.usuariosSession.put(usuarioSession.getUsuario(), usuarioSession);
32: }
33:
34: /**
35: * Cuando se cierra la sesión ya sea provocado o por timeout. Eliminar el
36: * usuario de sesión
37: *
38: * @param sessionId, IdSession que ha sido finalizada
39: */
40: public void sessionDestroyed(String sessionId) { 41: deleteUserBySessionId(sessionId);
42: }
43:
44: /**
45: * Eliminar el usuario del Map
46: *
47: * @param object
48: */
49: public void attributeRemoved(Object object) { 50: Usuario usuario = (Usuario) object;
51: this.usuariosSession.remove(usuario.getUsuario());
52: }
53:
54: /**
55: * Metodo privado para eliminar del Map, el usuario que estaba asociado a
56: * sessionId
57: *
58: * @param sessionId
59: */
60: private void deleteUserBySessionId(String sessionId) { 61: String usuarioToDelete = "";
62: for (UsuariosSession usuario : this.getUsuariosSession().values()) { 63: if (usuario.getIdSession().equals(sessionId)) { 64: usuarioToDelete = usuario.getUsuario();
65: }
66: }
67: if (!usuarioToDelete.equals("")) { 68: this.usuariosSession.remove(usuarioToDelete);
69: }
70:
71: }
72:
73: public ConcurrentHashMap<String, UsuariosSession> getUsuariosSession() { 74: return usuariosSession;
75: }
76:
77: public void setUsuariosSession(ConcurrentHashMap<String, UsuariosSession> usuariosSession) { 78: this.usuariosSession = usuariosSession;
79: }
80: }
A continuación un resumen de los diferentes aspectos que sería necesario declarar en el fichero de configuración de Struts “struts.xml”
A parte de la configuración necesaria para el correcto funcionamiento de la aplicación para implementar el control de sesiones, será necesario definir 2 interceptors y una salida genérica que mostrara la pagina de error que indica al usuario que ha perdido la sesión.
1: <interceptors>
3: <interceptor name="userSesionInterceptor" class="net.ejemplo.sesiones.interceptor.UserSessionInterceptor"/>
4:
5: <interceptor-stack name="securePppStack">
6: <interceptor-ref name="token"/>
7: <interceptor-ref name="userSesionInterceptor"/>
8: </interceptor-stack>
9:
10: </interceptors>
11:
12: <global-results>
13: <result name="userAnotherSesion" type="dispatcher">/WEB-INF/JSP/error/userAnotherSesion.jsp</result>
14: <result name="invalid.token" type="dispatcher">/WEB-INF/JSP/error/userAnotherSesion.jsp</result>
15: </global-results>
A continuación definimos el interceptor userSesionInterceptor que se encargara de validar el usuario que realiza la petición frente a los datos almacenados en el HashMap almacenado a nivel de aplicación.
1: package net.ejemplo.sesiones.interceptor;
2:
3: import com.ibm.db2.jcc.a.a;
4: import com.ibm.db2.jcc.am.no;
5: import com.ibm.db2.jcc.am.y;
6: import com.opensymphony.xwork2.ActionInvocation;
7: import com.opensymphony.xwork2.interceptor.Interceptor;
8: import java.util.Map;
9: import javax.servlet.http.HttpServletRequest;
10: import net.ejemplo.sesiones.EjemploConstants;
11: import net.ejemplo.sesiones.model.session.UsuariosSession;
12: import net.ejemplo.sesiones.model.usuario.Usuario;
13: import net.ejemplo.sesiones.service.SessionMonitor;
14: import net.ejemplo.sesiones.util.StringFunctions;;
15: import org.apache.commons.logging.Log;
16: import org.apache.commons.logging.LogFactory;
17: import org.apache.struts2.ServletActionContext;
18:
19:
20: /**
21: * Interceptor que verifica si el usuario en sesión es el correcto
22: * @author terinventer
23: */
24: public class UserSessionInterceptor implements Interceptor { 25:
26: protected Log log = LogFactory.getLog(UserSessionInterceptor.class);
27: SessionMonitor sessionMonitor;
28: // Salida generica, cuando el usuario ha iniciado una sesión en otro lugar
29: private String USER_ANOTHER_SESSION = "userAnotherSesion";
30:
31: public void destroy() { 32: }
33:
34: public void init() { 35: }
36:
37: public String intercept(ActionInvocation actionInvocation) throws Exception { 38:
39: Map session = actionInvocation.getInvocationContext().getSession();
40: String userSession = ((Usuario) session.get(EjemploConstants.USER_SESSION)).getUsuario();
41: HttpServletRequest request = ServletActionContext.getRequest();
42: String idSession = request.getSession().getId();
43:
44: if (sessionMonitor.getUsuariosSession() != null) { 45: // Si no contiene el usuario, asociado a ninguna Id sessión se permite el acceso
46: if (sessionMonitor.getUsuariosSession().containsKey(userSession)) { 47: UsuariosSession userMap = sessionMonitor.getUsuariosSession().get(userSession);
48: // si el usuario esta asociado a algun Id, se comprueba
49: if (!idSession.equals(userMap.getIdSession())) { 50: // Si las sesiones son diferentes y el usuario ya esta asociado a otra sesión, no se permite el acceso.
51: return USER_ANOTHER_SESSION;
52: }
53: }
54:
55: }
56: return actionInvocation.invoke();
57: }
58:
59: public SessionMonitor getSessionMonitor() { 60: return sessionMonitor;
61: }
62:
63: public void setSessionMonitor(SessionMonitor sessionMonitor) { 64: this.sessionMonitor = sessionMonitor;
65: }
66:
67: public void logInfo(String usuario, String msg) { 68: log.info("[" + StringUtil.fillLeft(usuario, ' ', 15) + "] " + msg); 69: }
70:
71: public void logDebug(String usuario, String msg) { 72: log.debug("[" + StringUtil.fillLeft(usuario, ' ', 15) + "] " + msg); 73: }
74:
75: public void logException(String usuario, String msg, Exception ex) { 76: String usr = StringUtil.fillLeft(usuario, ' ', 15);
77: log.error("[" + usr + "] " + msg + ex.getClass().getName() + ": " + ex.getMessage()); 78: for (StackTraceElement element : ex.getStackTrace()) { 79: log.error("[" + usr + "] Excepcion: " + element.getClassName() + " : " + element.getLineNumber()); 80: }
81: }
82: }
Hasta este punto, tendríamos controlado que el usuario no pueda tener iniciadas 2 sesiones en diferentes navegadores.
Para controlar el acceso de un usuario desde múltiples pestañas o ventanas, así como evitar que ejecute 2 peticiones al servidor simultáneamente. Se utilizara el interceptor token que implementa Struts.
Para definir todos los pasos y problemas encontrados con el uso de este interceptor vayan a la entrada de este blog:
Control de sesiones con Struts y Spring II
Espero sus opiniones o correcciones.
Saludos