Struts防止表单重复提交

1.什么是表单重复提交

> 在不刷新表单页面的前提下:
        >> 多次点击提交按钮
        >> 已经提交成功, 按 "回退" 之后, 再点击 "提交按钮".
        >> 在控制器响应页面的形式为转发情况下,若已经提交成功, 然后点击 "刷新(F5)"

> 注意:
        >> 若刷新表单页面, 再提交表单不算重复提交
        >> 若使用的是 redirect 的响应类型(地址栏发生变化), 已经提交成功后, 再点击 "刷新", 不是表单的重复提交

2.Struts解决表单重复提交问题

I. 在表单中添加 s:token 子标签

    > 生成一个隐藏域
    > 在 session 添加一个属性值
    > 隐藏域的值和 session 的属性值是一致的可以提交,否则认为是重复提交.
    
II. 使用 TokenTokenSession 拦截器.

    > 这两个拦截器均不在默认的拦截器栈中, 所以需要手工配置一下
    > 若使用 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 -->
 *
 * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
 *     &lt;interceptor-ref name="tokenSession/&gt;
 *     &lt;interceptor-ref name="basicStack"/&gt;
 *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 *
 * &lt;-- In this case, myMethod of the action class will not
 *        get checked for invalidity of token --&gt;
 * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
 *     &lt;interceptor-ref name="tokenSession&gt;
 *         &lt;param name="excludeMethods"&gt;myMethod&lt;/param&gt;
 *     &lt;/interceptor-ref name="tokenSession&gt;
 *     &lt;interceptor-ref name="basicStack"/&gt;
 *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 *
 * <!-- 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);
        }
    }

posted @ 2017-07-16 12:55  QiaoZhi  阅读(620)  评论(0编辑  收藏  举报