jueves, 12 de julio de 2012

Control de sesiones con Struts y Spring II

 

Esta entrada es continuación de Control de sesiones con Struts y Spring I es necesario leer la primera entrada para entender la actual.

Resumiendo muy brevemente, el uso token para struts.

El token permite ejecutar una acción una única vez, asociando una marca única asociada a una acción. La marca solo se actualizara una vez finalizada la acción en ejecución.

La utilización del token va a permitir que dentro de un navegador, se evite la ejecución de acciones desde diferentes ventanas o pestañas.

Aspectos a tener en cuenta a la hora de utilizar el token de struts y querer controlar todas las acciones dentro de la aplicación.

1.- Definir el interceptor correspondiente en struts.xml

   1: <interceptor-ref name="token"/>

2.- Peticiones por Post: Incluir la etiqueta <s:token/> en todos los formularios de la aplicación.


<s:form method="post" action="%{action2}" >
               <s:token/>



   1: <s:form method="post" action="EjemploSesionAction" >
   2:     <s:token/>
   3: </s:form>

3.- Peticiones por Get:


Definimos los enlaces de la siguiente forma:



   1: <s:url var="urlEjemplo" action="ejemploAction">  
   2:     <s:param name="struts.token.name">struts.token</s:param>
   3:     <s:param name="struts.token"><s:property value="struts.token"/></s:param> 
   4: </s:url>
   5: <s:a href="%{urlEjemplo}" id="ejemploAction">
   6:    <s:text name="ejemplo.sesion.enlace"
   7: &lt;/s:a>

 


4.- Problemas por RedirectAction y perdida de token:


Creamos una clase BaseAction extensible por todas las clases que ejecutan Action de la aplicación. Esta clase tendrá el parámetro “tokenRedirect” y el método getTokenRedirect.



   1: package net.ejemplo.sesiones.action;
   2:  
   3: import com.opensymphony.xwork2.ActionSupport;
   4: import org.apache.struts2.util.TokenHelper;
   5:  
   6: /**
   7:  *
   8:  * @author terinventer
   9:  */
  10: public class BaseAction extends ActionSupport {
  11:  
  12:     static final long serialVersionUID = 1l;
  13:     private String tokenRedirect;
  14:  
  15:     public BaseAction() {
  16:     }
  17:  
  18:     @Override
  19:     public String execute() throws Exception {
  20:         return SUCCESS;
  21:     }
  22:  
  23:  
  24:  
  25:     public String getTokenRedirect() {
  26:         
  27:        /**
  28:         * Actualizar el token para el action redirect.
  29:         */
  30:         String tokenAux = TokenHelper.generateGUID();
  31:         try {
  32:             session.put("struts.token", tokenAux);
  33:         } catch (IllegalStateException e) {
  34:             String msg = "Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: " + e.getMessage();
  35:             LOG.error(msg, e);
  36:             throw new IllegalArgumentException(msg);
  37:         }
  38:         
  39:         tokenRedirect = tokenAux;
  40:         return tokenRedirect;
  41:     }
  42:  
  43:     public void setTokenRedirect(String tokenRedirect) {
  44:         this.tokenRedirect = tokenRedirect;
  45:     }
  46: }

 


Tras esto, es necesario definir los redirect de la siguiente forma en struts.xml.



   1: <result type="redirectAction" name ="ejemploRedirect">
   2:     <param name="actionName">actionRedirect</param>
   3:     <param name="struts.token.name">struts.token</param>
   4:     <param name="struts.token">${tokenRedirect}</param>
   5: </result>

 


5.- Result <result type="stream">, Si queremos evitar perder el token a la hora de descargar un fichero utilizando este tipo de resultado para struts. Existe la opción de realizar la descarga mediante el uso de un iFrame oculto.



   1: <action name="Download" class="net.ejemplo.sesiones.action.DownloadAction">
   2:     <result type="stream">
   3:         <param name="contentType">contentType</param>
   4:         <param name="inputName">inputName</param>
   5:         <param name="contentDisposition">contentDisposition</param>
   6:         <param name="contentLength">contentLength</param>
   7:         <param name="bufferSize">1024</param>
   8:     </result>
   9: </action>

 


En la pagina sería necesario invocar a este action mediante la utilización de js.



   1: <script type="text/javascript">
   1:  
   2:     var elemIF = document.createElement("iframe");
   3:     elemIF.src = "Download.action";
   4:     elemIF.style.display = "none";
   5:     document.body.appendChild(elemIF);
</script>

Esto hará que se habrá el dialogo de descarga sin perder el correspondiente token.


 


Con todos estos aspectos, en principio tendríamos controlada la sesión de un usuario.


Como dije es una opción entre muchas, puede ser que falte algún aspecto que tratar.


El código fuente del proyecto se puede encontrar en código fuente o descargar pulsando el botón download.


 


Espero vuestras opiniones y comentarios.


Saludos

viernes, 6 de julio de 2012

Control de sesiones con Struts y Spring I

 

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