表单重复提交Double Submits

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

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

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

(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()维护一个请求队列。
[b]这种方式也只能防止“多次点击提交按钮”,对于其他的场景不适用![/b]

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

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

但是目前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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值