Tip Struts 2 #13: Control de formularios mediante token


VERSIONES

  1. 22/02/2014 (Primera publicación)
  2. 06/07/2014:
    • Añadido TokenSession

22/02/2014
struts2 tip

En numerosas ocasiones tenemos que implementar formularios en una aplicación web y evitar que se hagan múltiples submit del mismo debido a que se realizan operaciones delicadas, como por ejemplo la confirmación de una compra. Struts 2 incorpora un mecanismo para realizar este control de forma automática implementado en el inteceptor token y que además resulta útil para prevenir ataques de tipo CSRF. El funcionamiento es sencillo:

  1. Cada vez que se genera el formulario para enviarlo al navegador del usuario se incluye un campo oculto con un token (una cadena aleatoria) única por sesión.
  2. Al realizarse un submit del formulario, Struts 2 comprueba que el token existe y que para esa sesión no se haya realizado un submit de un formulario con el mismo token.
  3. Si el token del formulario es la primera vez que se envía, se realiza el flujo normal de Struts 2. En caso contrarío, se redirecciona al usuario hacia un result específico (invalid.token) y ni siquiera se llega a ejecutar el método del Action correspondiente al submit realizado. Asimismo, se guarda en los ActionError (ver tip 5) un mensaje de error.

Vamos a implementar un formulario en Struts 2 gestionado mediante token. Para ello, definimos el interceptor para el action hacia el cual se realiza el submit (el interceptor no viene activado en ninguna de las pilas predefinidas). Asimismo, será necesario definir el result invalid.token tal y como se ha comentado

struts.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
        "http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
	<constant name="struts.devMode" value="true" />
	<constant name="struts.custom.i18n.resources" value="global" />
	<!-- enable 'action' attribute -->
	<constant name="struts.mapper.action.prefix.enabled" value="false" />
	<!-- enable 'method'  attribute-->	
	<constant name="struts.enable.DynamicMethodInvocation" value="false" />

	<package name="default" namespace="/" extends="struts-default">

		<action name="initAction" class="com.danielme.tips.struts2.actions.FormAction" method="init">
			<result name="success">/jsp/form.jsp</result>
		</action>

		<action name="sendFormAction" class="com.danielme.tips.struts2.actions.FormAction" method="submit">
			<interceptor-ref name="token"/>
			<interceptor-ref name="defaultStack"/>            
            <result name="success">/jsp/result.jsp</result>
            <result name="invalid.token">/jsp/form.jsp</result>			
		</action>

	</package>

</struts>

En el formulario es imprescindible utilizar la etiqueta token para que se incluya el token asociado al formulario en un campo oculto.

<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
<html>
	<head>
		<%@include file="/jsp/head.jsp" %>
		<STYLE type="text/css">         
            .div-error
            {
                background-color: #ea9184;
                border: 2px solid #ea523b;
                font-weight: bold;
                padding: 6px 6px 6px 6px;
                text-align: left;
                font-size: 1em;
                width: 99%;
                margin-top: 10px;             
            }
            .div-error li
            {
                list-style-type: none;
            }
        </STYLE>
        
	</head>
	
	<body>
	
	    <s:actionerror cssClass="div-error"/>
        
		<s:form action="sendFormAction" method="POST" theme="simple">
	
			<s:text name='label'/>:<s:textfield name="field"/>
			
			<%-- print form token --%>
			<s:token />
			
			<s:submit value="%{getText('send')}"/>
			
		</s:form>
		<%@include file="/jsp/footer.jsp" %>
	</body>
</html>

En caso de token erróneo (ya se ha enviado previamente el formulario con el mismo token) tendremos en los ActionError un error correspondiente con el mensaje predefinido de Struts 2 struts.messages.invalid.token. Este mensaje puede ser modificado (ver tip #11):

title=tip 13 - Form token
label=Campo
send=Enviar
struts.messages.invalid.token=Formulario ya enviado

Para probar el funcionamiento del proyecto de ejemplo basta con enviar el formulario. El primer submit es correcto, pero si se vuelve al formulario con el botón atrás del navegador al hacerse un nuevo submit Struts 2 detecta que el token del formulario ya se ha enviado o «consumido» previamente por lo que se genera el error correspondiente y se recarga el formulario tal y como se ha indicado en el result invalid.token . Obsérvese que el formulario se ha vuelto a generar de nuevo incluyendo el token asociado por lo que si volvemos a realizar un submit en este caso será correcto.

Los errores en el token quedan registrados en el log de la aplicación con nivel WARN del siguiente modo:

WARN TokenHelper:56 - Form token 6U6DVGG066031COCBJPH9EMCLBTSB1HF does not match the session token null.

TokenSession Interceptor

Existe una especialización de este interceptor llamada tokenSession con un comportamiento distinto: si se realizan múltiples submit de un mismo formulario en lugar de mostrarse el result invalid.token se intenta mostrar el resultado del primer submit. Esto es fácil de ver en nuestro ejemplo si tras enviar el formulario volvemos al mismo con el botón back del navegador, cambiamos el texto del input y lo enviamos de nuevo; siempre veremos el resultado del primer submit.

El proyecto completo para Maven se encuentra en Github. Para más información sobre cómo utilizar GitHub, consultar este artículo.

<< TIPS STRUTS 2

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.