`
rensanning
  • 浏览: 3516851 次
  • 性别: Icon_minigender_1
  • 来自: 大连
博客专栏
Efef1dba-f7dd-3931-8a61-8e1c76c3e39f
使用Titanium Mo...
浏览量:37544
Bbab2146-6e1d-3c50-acd6-c8bae29e307d
Cordova 3.x入门...
浏览量:604538
C08766e7-8a33-3f9b-9155-654af05c3484
常用Java开源Libra...
浏览量:678399
77063fb3-0ee7-3bfa-9c72-2a0234ebf83e
搭建 CentOS 6 服...
浏览量:87443
E40e5e76-1f3b-398e-b6a6-dc9cfbb38156
Spring Boot 入...
浏览量:399961
Abe39461-b089-344f-99fa-cdfbddea0e18
基于Spring Secu...
浏览量:69113
66a41a70-fdf0-3dc9-aa31-19b7e8b24672
MQTT入门
浏览量:90598
社区版块
存档分类
最新评论

表单重复提交Double Submits

 
阅读更多
可能发生的场景:
  • 多次点击提交按钮
  • 刷新页面
  • 点击浏览器回退按钮
  • 直接访问收藏夹中的地址
  • 重复发送HTTP请求(Ajax)
  • CSRF攻击

(1)点击按钮后disable该按钮一会儿,这样能避免急躁的用户频繁点击按钮。
比如使用jQuery的话:
$("form").submit(function() {
  var self = this;
  $(":submit", self).prop("disabled", true);
  setTimeout(function() {
    $(":submit", self).prop("disabled", false);
  }, 10000);
});

这种方法有些粗暴,友好一点的可以把按钮文字变一下做个提示,比如Bootstrap的做法http://getbootstrap.com/javascript/#buttons
这种方式只能防止“多次点击提交按钮”,对于其他的场景不适用!

(2)不disable按钮,通过全局变量来控制多次点击(只有页面重新加载后可以再次点击)。
var isProcessing = false;
function doSignup() {
  if(isProcessing) {
    alert('The request is being submitted, please wait a moment...');
    return;
  }
  isProcessing = true;

  // do something......
}

需要注意的是如果是AJAX请求的话一定要在请求回来后重置这个值:
  $.ajax({
    url : "",
    complete :function() {
      isProcessing = false;
    },
    success :function(data) {
      //...
    }
  });

Ajax默认是异步请求,可以设置async为false后同步请求,或者采用jQuery的$.ajaxPrefilter()维护一个请求队列。
这种方式也只能防止“多次点击提交按钮”,对于其他的场景不适用!

(3)Post-Redirect-Get (PRG)
提交后发送一个redirect 请求,这样能避免用户按F5刷新页面,或浏览器的回退按钮
参考:http://en.wikipedia.org/wiki/Post/Redirect/Get
这种方式只能防止“刷新页面”,对于其他的场景不适用!

(4)Synchronizer Token Pattern
为每次请求生成一个唯一的Token,把它放入session和form的hidden。
处理前先校验token是否一致,不一致屏蔽请求,一致时立即清除后继续处理。
这种方法也适用于跨站请求伪造Cross-Site Request Forgery (CSRF)。
大部分Web框架都提供这方面的支持,比如:
  • Struts 1.x在Action类中可以通过saveToken(request)和isTokenValid(request)方法来实现。
  • Struts 2.x提供了org.apache.struts2.interceptor.TokenInterceptor来实现Token校验。
这种方式适用以上所有的场景!

但是目前Spring MVC还没有这方面的API,需要自己通过代码实现:

@Component
public class TokenHandler {
 
    @Autowired
    private org.springframework.cache.CacheManager cacheManager;
 
    public String generate() {
        Cache cache = cacheManager.getCache("tokens");
        String token = UUID.randomUUID().toString();
        cache.put(token, token);
        return token;
    }
 
}

public class TokenTag extends SimpleTagSupport {
 
    public void doTag() throws JspException, IOException {
        ServletContext servletContext = ((PageContext) this.getJspContext()).getServletContext();
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        TokenHandler handler = ctx.getBean(TokenHandler.class);
 
        JspWriter out = getJspContext().getOut();
        out.println("<input type=\"hidden\" id=\"token\" name=\"token\"  value=\"" + handler.generate() + "\""+ "/>");
        out.println("<input handler.generate="" hidden="" id="\" name="\" token="" type="\" value="\">");
   }
 
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
    boolean remove() default true;
}

@Component("tokenInterceptor")
public class TokenInterceptor extends HandlerInterceptorAdapter {
 
    private static final Logger logger = Logger.getLogger(TokenInterceptor.class);
 
    @Autowired
    private org.springframework.cache.CacheManager cacheManager;
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 
        boolean valid = true;
 
        HandlerMethod method = (HandlerMethod) handler;
 
        CheckToken annotation = method.getMethodAnnotation(CheckToken.class);
        if (annotation != null) {
            String token = request.getParameter("token");
            logger.info("token in the request <" + token + ">.");
            Cache cache = cacheManager.getCache("tokens");
            if (StringUtils.isEmpty(token)) {
                valid = false;
                logger.info("token not found in the request.");
            } else if (!StringUtils.isEmpty(token) && cache.get(token) != null && !token.equals(cache.get(token).get())) {
                valid = false;
                logger.info("token in the cache <" + cache.get(token) == null ? "" : cache.get(token).get() + ">.");
            } else {
                if (annotation.remove()) {
                    cache.evict(token);
                }
            }
 
            if (!valid) {
                logger.info("token is not valid , set error to request");
                request.setAttribute("error", "invalid token");
            }
        }
 
        return super.preHandle(request, response, handler);
    }
 
}

@Controller
public class TestController {
    @CheckToken
    @RequestMapping(value = "/test.htm", method = RequestMethod.POST)
    public String test(WebRequest request) {
        logger.info(request.getAttribute("error", WebRequest.SCOPE_REQUEST));
        return "index";
    }
}


<mvc:interceptors>
  <ref bean="tokenInterceptor"/>
</mvc:interceptors>
<bean class="org.springframework.cache.ehcache.EhCacheCacheManager" id="cacheManager">
  <property name="cacheManager" ref="ehcache"/>
</bean>
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" id="ehcache">
  <property name="configLocation" value="classpath:ehcache.xml">
    <property name="shared" value="true"/>
  </property>
</bean>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:nonamespaceschemalocation="http://ehcache.org/ehcache.xsd">
 <cache name="tokens" eternal="false" 
        maxelementsinmemory="1000" overflowtodisk="false" 
        timetoidleseconds="10" timetoliveseconds="10"/>
</ehcache>


<taglib version="2.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns="http://java.sun.com/xml/ns/j2ee" 
  xsi:schemalocation="http://java.sun.com/xml/ns/j2ee 
                      http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
 <description>Spring MVC</description>
 <tlib-version>4.0</tlib-version>
 <short-name>token</short-name>
 <uri>http://www.test.com/spring/mvc/token</uri>
 <tag>
  <name>token</name>
  <tag-class>com.test.spring.token.tags.TokenTag</tag-class>
  <body-content>empty</body-content>
 </tag>
</taglib>


<%@taglib prefix="tk" uri="http://www.junjun-dachi.org/spring/mvc/token"%>
<form>
<tk:token></tk:token>
</form>


参考:
http://www.zhihu.com/question/19805411
http://technoesis.net/prevent-double-form-submission/
http://stackoverflow.com/questions/2324931/duplicate-form-submission-in-spring
http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics