Struts2令牌拦截器示例
Struts 2令牌拦截器可用于处理多种表单提交问题。
在设计Web应用程序时,有时我们必须确保将重复提交的表单视为重复请求,而不进行处理。
例如,如果用户重新加载在线付款表单,但没有足够的支票将其识别为重复请求,则将向客户收取两次费用。
双重表单提交问题处理需要同时在客户端和服务器端进行。
在客户端,我们可以禁用"提交"按钮,"禁用"后退按钮,但是总会有一些选项供用户再次发送表单数据。
Struts2提供了旨在解决此特定问题的令牌拦截器。
Struts2令牌拦截器
在struts-default包中定义了两个拦截器:
<interceptor name="token" class="org.apache.struts2.interceptor.TokenInterceptor" <interceptor name="tokenSession" class="org.apache.struts2.interceptor.TokenSessionStoreInterceptor"
这些拦截器不属于任何预定义的拦截器堆栈,因为如果我们将其添加到任何操作中,则提交的表单应具有" token"参数,否则它将引发异常。
我们将通过一个简单的项目来了解它的用法。
最终的项目结构如下图所示。
Struts2令牌拦截器示例配置文件
web.xml
<?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>
部署描述符被配置为使用Struts 2框架。
pom.xml
<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>
该Web应用程序配置为maven项目,其中添加了" struts2-core"依赖项。
struts.xml
<?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.theitroad.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>
我们可以对任何动作使用"令牌"拦截器或者"令牌会话"拦截器。
如果"令牌"拦截器将请求标识为重复请求,则它将返回结果invalid.token,这就是我们为此配置结果的原因。
如果表单字段验证失败,那么将在我们从获取请求的地方返回相同页面的地方返回输入结果。
一旦看到重复请求的实现和应用程序行为,我们将研究完整的流程。
Struts2令牌拦截器示例操作类
UpdateUserAction.java
package com.theitroad.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); } }
一个具有基本表单域验证和一些Java bean属性的简单操作类。
请注意,更新时间是由操作类设置的,已添加它以显示当我们使用tokenSession拦截器时的应用程序行为。
Struts2令牌拦截器示例JSP页面
update.jsp
<%@ 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>
用户从此处提交表单以更新某些信息的应用程序的入口点。
我们正在使用actionerror标记来显示应用程序添加的任何验证错误。
需要注意的最重要一点是s:token
标记,令牌拦截器将使用它来确保重复请求不会被处理。
update_success.jsp
<%@ 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>
简单的JSP页面显示了操作类java bean属性。
invalid_token.jsp
<%@ 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>
简单的JSP页面显示了可能导致提交多个表单的不同方法,请注意actionerror标记的用法。
现在,当我们运行应用程序时,我们将以相同的顺序看到以下页面作为响应。
如果您查看输入页面的源代码,则会看到Struts2 API已将令牌标记转换为以下HTML代码段。
<input type="hidden" name="struts.token.name" value="token" <input type="hidden" name="token" value="HGWQI7ZGP7KFGJLDPNTSFHLUX5RF26IK"
您还会注意到以下日志片段。
Update Request Arrived to Action Class WARNING: Form token HGWQI7ZGP7KFGJLDPNTSFHLUX5RF26IK does not match the session token null.
请注意,重复请求甚至没有到达动作类,令牌拦截器返回invalid.token页面作为响应。
如果您将使用tokenSession拦截器,则会注意到它返回的响应与第一个请求相同。
您可以通过返回并编辑表单字段,然后再次提交表单来确认。
响应更新时间和字段值将是在第一个请求中发送的旧值。
Struts2令牌拦截器如何工作
现在,让我们看看令牌拦截器如何处理多种表单提交。
当请求更新操作时,Struts2标签API会生成一个唯一的令牌并将其设置为会话。
在HTML响应中将相同的令牌作为隐藏字段发送。提交带有令牌的表单后,令牌拦截器会拦截该表单,并尝试从会话中获取令牌并验证其是否与请求表单中收到的令牌相同。
如果在会话中找到令牌并进行了验证,则该请求将转发到链中的下一个拦截器。
令牌拦截器还会从会话中删除令牌。当再次提交相同的表单时,令牌拦截器将在会话中找不到它。
因此,它将添加操作错误消息并返回invalid.token结果作为响应。
您可以在上图中看到此消息,以获取invalid_token.jsp响应。
这样,令牌拦截器可确保带有令牌的表单仅被该操作处理一次。如果我们使用tokenSession拦截器,而不是返回无效的令牌响应,它将尝试返回与第一个具有相同令牌的操作所返回的响应相同的响应。
该实现是在" TokenSessionStoreInterceptor"类中完成的,该类保存会话中每个令牌的响应。我们可以通过i18n支持覆盖令牌拦截器通过" struts.messages.invalid.token"发送的操作错误消息。