1.什么是表单重复提交
> 在不刷新表单页面的前提下:
>> 多次点击提交按钮
>> 已经提交成功, 按 "回退" 之后, 再点击 "提交按钮".
>> 在控制器响应页面的形式为转发情况下,若已经提交成功, 然后点击 "刷新(F5)"
> 注意:
>> 若刷新表单页面, 再提交表单不算重复提交
>> 若使用的是 redirect 的响应类型(地址栏发生变化), 已经提交成功后, 再点击 "刷新", 不是表单的重复提交
2.Struts解决表单重复提交问题
I. 在表单中添加 s:token 子标签
> 生成一个隐藏域
> 在 session 添加一个属性值
> 隐藏域的值和 session 的属性值是一致的可以提交,否则认为是重复提交.
II. 使用 Token 或 TokenSession 拦截器.
> 这两个拦截器均不在默认的拦截器栈中, 所以需要手工配置一下
> 若使用 Token 拦截器, 则需要配置一个 token.valid 的 result
> 若使用 TokenSession 拦截器, 则不需要配置任何其它的 result
III. Token VS TokenSession
> 都是解决表单重复提交问题的
> 使用 token 拦截器会转到 token.valid 这个 result
> 使用 tokenSession 拦截器则还会响应那个目标页面, 但不会执行 tokenSession 的后续拦截器. 就像什么都没发生过一样!
IV. 可以使用 s:actionerror 标签来显示重复提交的错误消息.
该错误消息可以在国际化资源文件中覆盖. 该消息可以在 struts-messages.properties 文件中找到
3.token解决表单重复提交问题(可以跳转到指定页面显示指定消息)
1.Action同正常的Action一样
1 package FormRepeat; 2 import com.opensymphony.xwork2.ActionSupport; 3 public class FormRepeatSub extends ActionSupport { 4 private String username; 5 public String getUsername() { 6 return username; 7 } 8 public void setUsername(String username) { 9 this.username = username; 10 } 11 @Override 12 public String execute() throws Exception { 13 Thread.sleep(2000); 14 System.out.println(username); 15 return super.execute(); 16 } 17 }
2.JSP表单页面(表单中插入<s:token>)
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <%@ taglib uri="/struts-tags" prefix="s" %> 4 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 5 <html> 6 <head> 7 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 8 <title>Insert title here</title> 9 </head> 10 <body> 11 <form action="/Struts2FileUpload/formRepeatSub.action" method="post"> 12 <s:token></s:token> 13 username:<input type="text" name="username"> 14 <br/> 15 <input type="submit" value="提交"> 16 </form> 17 </body> 18 </html>
错误处理页面 TokenError.jsp
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <%@taglib uri="/struts-tags" prefix="s" %> 4 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 5 <html> 6 <head> 7 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 8 <title>Insert title here</title> 9 </head> 10 <body> 11 <!-- 用于struts排错,看出什么类型错误 --> 12 <s:debug></s:debug> 13 <!-- 显示错误的消息 --> 14 <s:actionerror/> 15 </body> 16 </html>
3.Struts的配置
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE struts PUBLIC 3 "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" 4 "http://struts.apache.org/dtds/struts-2.3.dtd"> 5 6 <struts> 7 8 <!-- <constant name="struts.devMode" value="true"></constant> --> 9 <package name="test2" extends="struts-default"> 10 <action name="formRepeatSub" class="FormRepeat.FormRepeatSub"> 11 <!-- 使用token拦截器 --> 12 <interceptor-ref name="token"></interceptor-ref> 13 <!-- 使用系统默认拦截 --> 14 <interceptor-ref name="defaultStack"></interceptor-ref> 15 <!-- 正常提交跳转的页面 --> 16 <result>/message.jsp</result> 17 <!-- 重复提交后跳转的页面 --> 18 <result name="invalid.token">/TokenError.jsp</result> 19 </action> 20 </package> 21 </struts>
国际化资源文件:
测试:
总结:
使用token的防止表单提交在form表单的开头添加一个token,添加后可以在页面查看源码生成一个随机的value值。根据session的进行匹配,每次都产生一个随机值,相同可以提交,不同就认为是重复提交。在action配置的时候添加token拦截器,需要配置一个 token.valid 的 result,结果转发到页面出错的页面。如下
在页面出错的页面(TokenError.jsp),获取出错消息(前提是导入struts标签<%@taglib uri="/struts-tags" prefix="s" %>)
页面出错的消息可以在国际化资源文件(i18n.properties)中设置, 该消息可以在 struts-messages.properties 文件中找到并修改
struts.messages.invalid.token=^^The form has already been processed or no token was supplied, please try again.
测试:
(1)正常可以跳转
(2)提交后后退:
提交前:
提交后后退:
(3)刷新也是重复提交
4.tokenSession解决表单重复提交问题(还会响应那个目标页面, 但不会执行 tokenSession 的后续拦截器. 就像什么都没发生过一样!)
只需要修改一下Ation配置,将拦截器设置为tokenSession ,重复提交后不会出提示,但数据只提交一次。
1 <action name="formRepeatSub" class="FormRepeat.FormRepeatSub"> 2 <!-- 改为tokenSession拦截器,这个一般放在前面。出错后不会执行后续拦截器,提高性能 --> 3 <interceptor-ref name="tokenSession"></interceptor-ref> 4 <!-- 使用默认拦截器 --> 5 <interceptor-ref name="defaultStack"></interceptor-ref> 6 <!-- 跳转的页面,不会做处理,相当于只提交一次 --> 7 <result>/message.jsp</result> 8 </action>
页面插入一个<s:token></s:token>
1 <form action="/Struts2FileUpload/formRepeatSub.action" method="post"> 2 <s:token></s:token> 3 username:<input type="text" name="username"> 4 <br/> 5 <input type="submit" value="提交"> 6 </form>
这个多次点也提交一次,也不会跳转,也不会给出提示,使用简单。只需要在表单第一个元素添加一个<s:token></s:token>,然后配置一下拦截器。
查看TokenSession源码:
大概是进来之后首先调用handleToken()判断session中是否有id为token值的token,没有就调用handleValidToken()获取到token的name与值并存入map缓存起来,如果有就走handleInvalidToken()终止方法的执行。
/* * $Id$ * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.struts2.interceptor; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.Result; import com.opensymphony.xwork2.util.ValueStack; import org.apache.struts2.ServletActionContext; import org.apache.struts2.util.InvocationSessionStore; import org.apache.struts2.util.TokenHelper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Map; /** * <!-- START SNIPPET: description --> * * This interceptor builds off of the {@link TokenInterceptor}, providing advanced logic for handling invalid tokens. * Unlike the normal token interceptor, this interceptor will attempt to provide intelligent fail-over in the event of * multiple requests using the same session. That is, it will block subsequent requests until the first request is * complete, and then instead of returning the <i>invalid.token</i> code, it will attempt to display the same response * that the original, valid action invocation would have displayed if no multiple requests were submitted in the first * place. * * <p/> * * <b>NOTE:</b> As this method extends off MethodFilterInterceptor, it is capable of * deciding if it is applicable only to selective methods in the action class. See * <code>MethodFilterInterceptor</code> for more info. * * <!-- END SNIPPET: description --> * * <p/> <u>Interceptor parameters:</u> * * <!-- START SNIPPET: parameters --> * * <ul> * * <li>None</li> * * </ul> * * <!-- END SNIPPET: parameters --> * * <p/> <u>Extending the interceptor:</u> * * <p/> * * <!-- START SNIPPET: extending --> * * There are no known extension points for this interceptor. * * <!-- END SNIPPET: extending --> * * <p/> <u>Example code:</u> * * <pre> * <!-- START SNIPPET: example --> * * <action name="someAction" class="com.examples.SomeAction"> * <interceptor-ref name="tokenSession/> * <interceptor-ref name="basicStack"/> * <result name="success">good_result.ftl</result> * </action> * * <-- In this case, myMethod of the action class will not * get checked for invalidity of token --> * <action name="someAction" class="com.examples.SomeAction"> * <interceptor-ref name="tokenSession> * <param name="excludeMethods">myMethod</param> * </interceptor-ref name="tokenSession> * <interceptor-ref name="basicStack"/> * <result name="success">good_result.ftl</result> * </action> * * <!-- END SNIPPET: example --> * </pre> * */ public class TokenSessionStoreInterceptor extends TokenInterceptor { private static final long serialVersionUID = -9032347965469098195L; @Override protected String handleToken(ActionInvocation invocation) throws Exception { //see WW-2902: we need to use the real HttpSession here, as opposed to the map //that wraps the session, because a new wrap is created on every request HttpSession session = ServletActionContext.getRequest().getSession(true); synchronized (session.getId().intern()) { if (!TokenHelper.validToken()) { return handleInvalidToken(invocation); } return handleValidToken(invocation); } } @Override protected String handleInvalidToken(ActionInvocation invocation) throws Exception { ActionContext ac = invocation.getInvocationContext(); HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST); HttpServletResponse response = (HttpServletResponse) ac.get(ServletActionContext.HTTP_RESPONSE); String tokenName = TokenHelper.getTokenName(); String token = TokenHelper.getToken(tokenName); if ((tokenName != null) && (token != null)) { Map params = ac.getParameters(); params.remove(tokenName); params.remove(TokenHelper.TOKEN_NAME_FIELD); String sessionTokenName = TokenHelper.buildTokenSessionAttributeName(tokenName); ActionInvocation savedInvocation = InvocationSessionStore.loadInvocation(sessionTokenName, token); if (savedInvocation != null) { // set the valuestack to the request scope ValueStack stack = savedInvocation.getStack(); request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack); ActionContext savedContext = savedInvocation.getInvocationContext(); savedContext.getContextMap().put(ServletActionContext.HTTP_REQUEST, request); savedContext.getContextMap().put(ServletActionContext.HTTP_RESPONSE, response); Result result = savedInvocation.getResult(); if ((result != null) && (savedInvocation.getProxy().getExecuteResult())) { result.execute(savedInvocation); } // turn off execution of this invocations result invocation.getProxy().setExecuteResult(false); return savedInvocation.getResultCode(); } } return INVALID_TOKEN_CODE; } @Override protected String handleValidToken(ActionInvocation invocation) throws Exception { // we know the token name and token must be there String key = TokenHelper.getTokenName(); String token = TokenHelper.getToken(key); String sessionTokenName = TokenHelper.buildTokenSessionAttributeName(key); InvocationSessionStore.storeInvocation(sessionTokenName, token, invocation); return invocation.invoke(); } }
@Override
protected String handleToken(ActionInvocation invocation) throws Exception {
//see WW-2902: we need to use the real HttpSession here, as opposed to the map
//that wraps the session, because a new wrap is created on every request
HttpSession session = ServletActionContext.getRequest().getSession(true);
synchronized (session.getId().intern()) {
if (!TokenHelper.validToken()) {
return handleInvalidToken(invocation);
}
return handleValidToken(invocation);
}
}