Struts 2 token interceptor can be used to handle multiple form submission problem. While designing web application, sometimes we have to make sure that double form submission is treated as duplicate request and not be processed. For example, if user reloads the online payment form and there are not enough checks in place to identify it as duplicate request, customer will be charged twice.
Double form submission problem handling needs to be done both at client side and server side. In client side, we can disable the submit button, disable back button but there will always be options through which user can send the form data again. Struts2 provides token interceptors that are designed to deal with this particular problem.
Struts2 Token Interceptor
There are two interceptors defined in struts-default
package as:
1 2 3 4 |
<interceptor name="token" class="org.apache.struts2.interceptor.TokenInterceptor"/> <interceptor name="tokenSession" class="org.apache.struts2.interceptor.TokenSessionStoreInterceptor"/> |
These interceptors are not part of any predefined interceptor stack because if we add it for any action, the form submitted should have a token
parameter else it will throw exception. We will look it’s usage with a simple project. Final project structure will look like below image.
Struts2 Token Interceptor Example Configuration Files
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://java.sun.com/xml/ns/javaee" xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>Struts2TokenInterceptor</display-name> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> |
Deployment descriptor is configured to use Struts 2 framework.
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>Struts2TokenInterceptor</groupId> <artifactId>Struts2TokenInterceptor</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <warSourceDirectory>WebContent</warSourceDirectory> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> <finalName>${project.artifactId}</finalName> </build> <dependencies> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.15.1</version> </dependency> </dependencies> </project> |
The web application is configured as maven project where we have added struts2-core
dependency.
struts.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "https://struts.apache.org/dtds/struts-2.3.dtd"> <struts> <!-- constant to define result path locations to project root directory --> <constant name="struts.convention.result.path" value="/"></constant> <package name="user" namespace="/" extends="struts-default"> <action name="update"> <result>/update.jsp</result> </action> <action name="UpdateUser" class="com.journaldev.struts2.actions.UpdateUserAction"> <interceptor-ref name="token"></interceptor-ref> <!-- OR <interceptor-ref name="tokenSession"></interceptor-ref> --> <interceptor-ref name="defaultStack"></interceptor-ref> <result name="success">/update_success.jsp</result> <result name="input">/update.jsp</result> <result name="invalid.token">/invalid_token.jsp</result> </action> </package> </struts> |
- We can use either
token
interceptor ortokenSession
interceptor with any action. - If
token
interceptor identifies the request as duplicate, then it returns the result invalid.token, that’s why we have a result configured for this. - If form field validation fails then input result is returned where we are returning the same page from where we get the request.
We will look into the complete flow once we have seen the implementation and application behavior with duplicate request.
Struts2 Token Interceptor Example Action Class
UpdateUserAction.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package com.journaldev.struts2.actions; import java.util.Date; import com.opensymphony.xwork2.ActionSupport; public class UpdateUserAction extends ActionSupport { @Override public String execute() { System.out.println("Update Request Arrived to Action Class"); //setting update time in action class setUpdateTime(new Date()); return SUCCESS; } @Override public void validate(){ if(isEmpty(getName())){ addActionError("Name can't be empty"); } if(isEmpty(getAddress())){ addActionError("Address can't be empty"); } } //java bean variables private String name; private String address; private Date updateTime; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } private boolean isEmpty(String str) { return str == null ? true:(str.equals("") ? true:false); } } |
A simple action class with basic form fields validation and some java bean properties. Notice that update time is set by action class, it has been added to show the application behavior when we use tokenSession interceptor.
Struts2 Token Interceptor Example JSP Pages
update.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <%@ taglib uri="/struts-tags" prefix="s" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Update User Request Page</title> </head> <body> <s:if test="hasActionErrors()"> <s:actionerror/> </s:if> <br> <s:form action="UpdateUser"> <s:textfield name="name" label="User Name"></s:textfield> <s:textfield name="address" label="Address"></s:textfield> <s:submit name="submit" value="Update"></s:submit> <%-- add token to JSP to be used by Token interceptor --%> <s:token /> </s:form> </body> </html> |
The entry point of the application from where user will submit form to update some information. We are using actionerror tag to show any validation errors added by the application. The most important point to note is s:token
tag that will be used by token interceptors in making sure duplicate requests are not getting processed.
update_success.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <%@ taglib uri="/struts-tags" prefix="s" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Update User Success Page</title> </head> <body> <h3>User information updated successfully.</h3> Name: <s:property value="name"/><br> Address: <s:property value="address"/><br> Update Time: <s:date name="updateTime"/><br> <h4>Thank You!</h4> </body> </html> |
Simple JSP page showing action class java bean properties.
invalid_token.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <%@ taglib uri="/struts-tags" prefix="s" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Update Duplicate Request Page</title> </head> <body> <h3>User information is not updated, duplicate request detected.</h3> <h4>Possible Reasons are:</h4> <ul> <li>Back button usage to submit form again</li> <li>Double click on Submit button</li> <li>Using "Reload" Option in browser</li> </ul> <br> <s:if test="hasActionErrors()"> <s:actionerror/> </s:if> </body> </html> |
Simple JSP page showing different methods that can cause multiple form submissions, notice the actionerror tag usage.
Now when we will run our application, we will see following pages as response in the same order.
If you will look into the source of input page, you will see that Struts2 API has converted token tag to following HTML snippet.
1 2 3 4 |
<input type="hidden" name="struts.token.name" value="token" /> <input type="hidden" name="token" value="HGWQI7ZGP7KFGJLDPNTSFHLUX5RF26IK" /> |
Also you will notice following logs snippet.
1 2 3 4 |
Update Request Arrived to Action Class WARNING: Form token HGWQI7ZGP7KFGJLDPNTSFHLUX5RF26IK does not match the session token null. |
Notice that duplicate request doesn’t even reach to action class and token interceptor returns the invalid.token page as response.
If you will use tokenSession interceptor, you will notice that it returns the same response as the first request. You can confirm this by going back and edit form fields and then submitting form again. The response update time and field values will be old values as sent in the first request.
How Struts2 Token Interceptor Works
Now let’s see how token interceptor works to handle multiple form submissions.
- When a request is made to the update action, Struts2 tags API generates a unique token and set it to the session. The same token is sent in the HTML response as hidden field.
- When the form is submitted with token, it is intercepted by token interceptor where it tries to fetch the token from the session and validate that it’s same as the token received in the request form. If token is found in session and validated then the request is forwarded to the next interceptor in the chain. Token interceptor also removes the token from the session.
- When the same form is submitted again, token interceptor will not find it in the session. So it will add an action error message and return invalid.token result as response. You can see this message in above image for invalid_token.jsp response. This way token interceptor make sure that a form with token is processed only once by the action.
- If we use tokenSession interceptor, rather than returning invalid token response, it tries to return the same response as the returned by the first action with same token. This implementation is done in the
TokenSessionStoreInterceptor
class that saves the response for each token in the session. - We can override the action error message sent by token interceptor through i18n support with key as “struts.messages.invalid.token”.
Thats all for the usage of Struts2 token interceptor to handle multiple form submission problem in web application. Download the application from below link and play around with it for better understanding.