- El
formulario - La rutina
transaccional - Limpiar el
token - Librería de
TAGS - El tag de
generación de token - El tag de
Verificación/desactivación - El tag de
reactivación - Control de transacciones y
Struts
Cuando realizamos una aplicación Web, hay un
problema bastante común y se produce cuando el usuario
realiza una transacción:
- Hace un doble click muy rápido sobre un
botón invocando doblemente transacciones - Se pone nervioso y, antes de que termine, la vuelve a
invocar. - Se produce un error y no está seguro de que
las transacciones se hayan confirmado por lo que lo reintenta
para ver que le dice el sistema.. - Pulsa el botón volver, refrescar y avanzar con
lo que reenvía una misma transacción dos
veces.
Una de las soluciones,
que habitualmente se utiliza, es escribir un pequeño
código
JavaScript que
nos permita evitar la situación (al menos
parcialmente).
El primer problema de esta aproximación es
conceptual: Jamás me debo fiar de las validaciones
realizadas en el cliente Web
(aparte de la parcialidad de la solución)
porque:
- Son fácilmente evitables.
- JavaScript depende de la versión del navegador
(y sistema
operativo) que complica la mantenibilidad. - JavaScript puede desactivarse.
- Podemos hacer un programas que
automatice una petición HTTP sin que
exista un navegador (y empezad a pensar en
WebServices)
Las validaciones importantes hay que controlarlas
siempre en el servidor (aunque
las complementemos en el cliente para evitar flujos innecesarios
de información).
Tenemos que entender los posibles estados que podemos
tener el problema:
- Un usuario llega a un punto que donde se autoriza una
nueva transacción (se activa un ticket o
token) - El usuario ejecuta una vez esa transacción y
se deshabilita el token - El usuario para poder
ejecutar esa misma transacción de nuevo tiene que llegar
a un punto que le vuelva a habilitar el token
El párrafo
anterior está escrito en un lenguaje poco
técnico lo que provoca ambigüedades .
Podríamos tratar de utilizar UML para
concretar un poquito más los posibles estados y tratar de
plasmar en el futuro las posibles variaciones.
Este problema lo podríamos tener tanto en una
aplicación con interfaz tipo Windows o en
una aplicación Web ….. Lo lógico sería
tratar de construir una librería que pudiera se invocada
por distintos tipos de aplicaciones.
Nosotros no vamos a ir tan allá pero trataremos
de dar el primer paso en la solución creando unas
librerías de etiquetas básicas para resolverlo en
aplicaciones Web/JSP.
Construiremos un pequeño ejemplo que veremos
elemento a elemento.
Partimos de un formulario donde mandaremos al servidor
un campo oculto. En este campo se almacena un valor que solo
es utilizable una vez. El encargado de generar y validar este
parámetro es siempre el servidor y lo hacemos a
través de una etiqueta. Pasamos como parámetro el
nombre de la transacción (ejemplo) para poder tener
tantos tokens activos como
deseemos.
<controltokens:generaToken
nombreTransaccion="ejemplo"/>
<%@page contentType="text/html"%> <%@taglib uri="/roberto/tokens" <html> <head><title>JSP <body> <center> <form Nombre <input type="text" <input type="HIDDEN" value='<controltokens:generaToken <br><input type="submit" </form> <a href="./anulaToken.jsp">Pulsame para </center> </body> </html> |
Podemos ver el aspecto de la página
Y el código que llegaría a nuestro
navegador. Hemos elegido como token la fecha actual pero
podría ser cualquier cosa
<html> <head><title>JSP <body> <center> <form Nombre <input type="text" <input type="HIDDEN" value='Thu Nov 25 23:15:59 CET <br><input type="submit" </form> <a href="./anulaToken.jsp">Pulsame para </center> </body> </html> |
Cuando ejecutemos la transacción debemos
verificar que el toquen es válido. Usamos otro
JSP.
<controltokens:compruebaToken
nombreTransaccion="ejemplo"/>
Si no es así, generamos una excepción y
redirigimos a una página de error
errorPage="/pages/transaccionDuplicada.jsp"
<%@page contentType="text/html" <%@taglib uri="/roberto/tokens" <html> <head><title>Transaccion <body> <controltokens:compruebaToken Soy la página y he funcionado —– Aquí debe ir el código de la </body> </html> |
Si la ejecutamos una vez
Si ejecutamos la segunda vez (con refrescar, volver y
enviar, volver-refrescar y enviar).
En el caso de querer volver a ejecutar la
transacción deberíamos pasar por una página
que rehabilitase el token.
<controltokens:anulaToken
nombreTransaccion="ejemplo"/>
<%@page contentType="text/html" <%@taglib uri="/roberto/tokens" <html> <head><title>Limpieza de <body> <controltokens:anulaToken <a href="./formulariobasico.jsp">Pulsame </body> </html> |
Ahora solo tenemos que revisar el tutorial
donde
os mostrábamos como crear paso a paso una etiqueta
y analizar el código particular.
package tokens; import java.io.IOException; import java.io.PrintWriter; import import import javax.servlet.jsp.JspWriter; import import import import import import import import java.util.*; /** * Generated tag class. */ public class GeneraTokenTag extends TagSupport /** property declaration for tag attribute: * */ private String nombreTransaccion; public GeneraTokenTag() { super(); } //////////////////////////////////////////////////////////////// /// /// /// User methods. /// /// /// /// Modify these methods to customize your tag /// /// //////////////////////////////////////////////////////////////// void depura(String mensaje) { System.out.println("GeneraTokenTag: " + } // // methods called from doStartTag() // /** * * Fill in this method to perform other * */ public void otherDoStartTagOperations() // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // // For example, to print something out to the // try { depura("Cogemos el output"); JspWriter out = depura("Recuperamos el valor de la // recuperamos el valor de la Object tokenActual = String texto = null; if (tokenActual == null) { depura("Generamos el token porque no esta en // el token no esta en memoria Date fecha = new Date(); texto = fecha.toString(); pageContext.getSession().setAttribute(nombreTransaccion,texto); } else { depura("El token existe"); // volvemos a poner el valor en if(tokenActual.toString().compareTo("nulo")==0) { texto = "incorrecto"; } else { texto = tokenActual.toString(); } } // generamos la respuesta out.print(texto); out.flush(); } catch (java.io.IOException ex) { // do something } } /** * * Fill in this method to determine if the tag * Called from doStartTag(). * */ public boolean theBodyShouldBeEvaluated() // // TODO: code that determines whether the body // evaluated should be placed here. // Called from the doStartTag() // return true; } // // methods called from doEndTag() // /** * * Fill in this method to perform other * */ public void otherDoEndTagOperations() // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // } /** * * Fill in this method to determine if the rest * should be generated after this tag is * Called from doEndTag(). * */ public boolean // // TODO: code that determines whether the rest // should be evaluated after the tag is // should be placed here. // Called from the doEndTag() // return true; } //////////////////////////////////////////////////////////////// /// /// /// Tag Handler interface methods. /// /// /// Do not modify these methods; instead, /// methods that they call. /// /// /// //////////////////////////////////////////////////////////////// /** . * * This method is called when the JSP engine * after the attributes are * Scripting variables (if any) have their * @return EVAL_BODY_INCLUDE if the JSP should evaluate the tag body, otherwise return * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doStartTag() throws JspException, otherDoStartTagOperations(); if (theBodyShouldBeEvaluated()) { return EVAL_BODY_INCLUDE; } else { return SKIP_BODY; } } /** . * * * This method is called after the JSP engine * @return EVAL_PAGE if the JSP engine should the JSP page, otherwise return * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doEndTag() throws JspException, otherDoEndTagOperations(); if (shouldEvaluateRestOfPageAfterEndTag()) return EVAL_PAGE; } else { return SKIP_PAGE; } } public String getNombreTransaccion() return nombreTransaccion; } public void setNombreTransaccion(String value) nombreTransaccion = value; } } |
El tag de
Verificación/desactivación
package tokens; import java.io.IOException; import java.io.PrintWriter; import import import javax.servlet.jsp.JspWriter; import import import import import import import import java.util.*; /** * Generated tag class. */ public class CompruebaTokenTag extends /** property declaration for tag attribute: * */ private String nombreTransaccion; public CompruebaTokenTag() { super(); } //////////////////////////////////////////////////////////////// /// /// /// User methods. /// /// /// /// Modify these methods to customize your tag /// /// //////////////////////////////////////////////////////////////// void depura(String mensaje) { System.out.println("CompruebaTokenTag: " + } // // methods called from doStartTag() // /** * * Fill in this method to perform other * */ public void otherDoStartTagOperations() throws // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // // For example, to print something out to the // try { JspWriter out = // recuperamos el valor de la Object tokenActual = String texto = null; if (tokenActual != null) { depura("El token no es nulo"); // el token no esta en memoria Object tokenPasadoEnJSP = if(tokenPasadoEnJSP == null) { depura("El token es nulo"); throw new JspException("El token no se ha } if { depura("El token no coincide con el throw new JspException("El token no coincide } depura("El token coincide y ponemos un nuevo pageContext.getSession().setAttribute(nombreTransaccion,"nulo"); } else { // volvemos a poner el valor en depura("No hay token en sesion"); throw new JspException("No hay token en } // generamos la respuesta out.print("El token es correcto"); out.flush(); } catch (java.io.IOException ex) { // do something } } /** * * Fill in this method to determine if the tag * Called from doStartTag(). * */ public boolean theBodyShouldBeEvaluated() // // TODO: code that determines whether the body // evaluated should be placed here. // Called from the doStartTag() // return true; } // // methods called from doEndTag() // /** * * Fill in this method to perform other * */ public void otherDoEndTagOperations() // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // } /** * * Fill in this method to determine if the rest * should be generated after this tag is * Called from doEndTag(). * */ public boolean // // TODO: code that determines whether the rest // should be evaluated after the tag is // should be placed here. // Called from the doEndTag() // return true; } //////////////////////////////////////////////////////////////// /// /// /// Tag Handler interface methods. /// /// /// Do not modify these methods; instead, /// methods that they call. /// /// /// //////////////////////////////////////////////////////////////// /** . * * This method is called when the JSP engine * after the attributes are * Scripting variables (if any) have their * @return EVAL_BODY_INCLUDE if the JSP engine otherwise return SKIP_BODY. * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doStartTag() throws JspException depura("Llegamos a la etiqueta"); otherDoStartTagOperations(); if (theBodyShouldBeEvaluated()) { return EVAL_BODY_INCLUDE; } else { return SKIP_BODY; } } /** . * * * This method is called after the JSP engine * @return EVAL_PAGE if the JSP engine should otherwise return SKIP_PAGE. * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doEndTag() throws JspException otherDoEndTagOperations(); if (shouldEvaluateRestOfPageAfterEndTag()) return EVAL_PAGE; } else { return SKIP_PAGE; } } public String getNombreTransaccion() return nombreTransaccion; } public void setNombreTransaccion(String value) nombreTransaccion = value; } } |
package tokens; import java.io.IOException; import java.io.PrintWriter; import import import javax.servlet.jsp.JspWriter; import import import import import import import /** * Generated tag class. */ public class AnulaTokenTag extends TagSupport /** property declaration for tag attribute: * */ private String nombreTransaccion; public AnulaTokenTag() { super(); } //////////////////////////////////////////////////////////////// /// /// /// User methods. /// /// /// /// Modify these methods to customize your tag /// /// //////////////////////////////////////////////////////////////// void depura(String mensaje) { System.out.println("AnulaTokenTag: " + } // // methods called from doStartTag() // /** * * Fill in this method to perform other * */ public void otherDoStartTagOperations() // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // // For example, to print something out to the // try { JspWriter out = out.println("Borramos el token " + pageContext.getSession().removeAttribute(nombreTransaccion); // generamos la respuesta out.print("Token anulado"); out.flush(); } catch (java.io.IOException ex) { // do something } } /** * * Fill in this method to determine if the tag * Called from doStartTag(). * */ public boolean theBodyShouldBeEvaluated() // // TODO: code that determines whether the body // evaluated should be placed here. // Called from the doStartTag() // return true; } // // methods called from doEndTag() // /** * * Fill in this method to perform other * */ public void otherDoEndTagOperations() // // TODO: code that performs other operations // should be placed here. // It will be called after initializing // finding the parent, setting IDREFs, etc, // before calling // } /** * * Fill in this method to determine if the rest * should be generated after this tag is * Called from doEndTag(). * */ public boolean // // TODO: code that determines whether the rest // should be evaluated after the tag is // should be placed here. // Called from the doEndTag() // return true; } //////////////////////////////////////////////////////////////// /// /// /// Tag Handler interface methods. /// /// /// Do not modify these methods; instead, /// methods that they call. /// /// /// //////////////////////////////////////////////////////////////// /** . * * This method is called when the JSP engine * after the attributes are * Scripting variables (if any) have their * @return EVAL_BODY_INCLUDE if the JSP engine otherwise return SKIP_BODY. * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doStartTag() throws JspException, otherDoStartTagOperations(); if (theBodyShouldBeEvaluated()) { return EVAL_BODY_INCLUDE; } else { return SKIP_BODY; } } /** . * * * This method is called after the JSP engine * @return EVAL_PAGE if the JSP engine should otherwise return SKIP_PAGE. * This method is automatically generated. Do * Instead, modify the methods that this method * * */ public int doEndTag() throws JspException, otherDoEndTagOperations(); if (shouldEvaluateRestOfPageAfterEndTag()) return EVAL_PAGE; } else { return SKIP_PAGE; } } public String getNombreTransaccion() return nombreTransaccion; } public void setNombreTransaccion(String value) nombreTransaccion = value; } } |
No olvidar modificar el fichero web.xml para
poder utilizar las tags en nuestras páginas
JSP
…….. <taglib> <taglib-uri>/roberto/tokens</taglib-uri> <taglib-location>/WEB-INF/tokens.tld</taglib-location> </taglib> </web-app> |
Como habréis podido observar el efecto es el
deseado aunque todavía quedarían por dar muchos
pasos:
- Deberíamos construir un juego de
pruebas
detallado que nos garantizase su correcto comportamiento. - Como segundo paso deberíamos sacar del
código de las TAGs a librerías más
genéricas para utilizarlas en aplicaciones tipo MVC y
otro tipo de interfaces. - Como tercer paso, deberíamos ligar estas
capacidades dentro de nuestro FrameWorks para que incluso se
generase automáticamente el código redundante en
el cliente - Y aún podríamos hacer más
cosas….
Control de transacciones y Struts
Mucha gente utiliza Struts
pensando que el control de
transacciones y doble clicks ya esta solucionado…. pero no es
así de automático.
En la acción
que deseamos que active el control transaccional (previo al
formulario) debemos llamar a la función:
saveToken(httpServletRequest);
Los formularios
tenemos que montarlos con la etiqueta de Struts html
<%@ taglib uri="/tags/struts-bean" <%@ taglib uri="/tags/struts-html" <html> <head> <title></title> </head> <body> <center> <h2>Introduzca los datos</h2> <hr width='60%'> <html:form action='/primeraAccion' <br>Usuario: <html:text <br>Password: <html:password <br><html:submit </html:form> <br> <html:errors/> </center> </body> </html> |
Podemos ver que pasa algo similar a lo que hicimos con
nuestras tags
<html> <head> <title></title> </head> <body> <center> <h2>Introduzca los <hr width='60%'> <form name="losdatos" method="post" <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="2d5b61e821accd2a449299ee78055b8f"> <br>Usuario: <input type="text" <br>Password: <input type="password" <br><input type="submit" </form> <br> <UL><LI>Hay un problema con la </center> </body> </html> |
Posteriormente en la acción que ejecuta la
transacción podemos verificar el token e invalidarlo
con:
if(
this.isTokenValid(httpServletRequest)==true)
Y luego limpiarlo con
this.resetToken(httpServletRequest);
El desarrollo de
aplicaciones Web es un arte bastante
compleja aunque aparentemente pudiera parecer lo contrario
Uno de los problemas es
que la misma cosa se puede hacer de muchos modos y es
fácil que elijamos uno poco afortunado.
Este ejemplo no soluciona todos los problemas pero
sienta unas bases para el estudio y solución del
mismo..
Si estudiáis metodologías tipo Programación Extrema, observareis que uno
de los principios a
tener en cuenta es realizar diseños sencillos. Esto
significa (y es una posible interpretación personal) que no
debemos diseñar una solución pensando en los
problemas que tendremos dentro de dos años sino los que
tendremos dentro de dos meses… Dentro de dos años ya
veremos….
Roberto Canales Mora
www.adictosaltrabajo.com