可能发生的场景:
[list][*]多次点击提交按钮
[*]刷新页面
[*]点击浏览器回退按钮
[*]直接访问收藏夹中的地址
[*]重复发送HTTP请求(Ajax)
[*]CSRF攻击[/list]
(1)点击按钮后disable该按钮一会儿,这样能避免急躁的用户频繁点击按钮。
比如使用jQuery的话:
这种方法有些粗暴,友好一点的可以把按钮文字变一下做个提示,比如Bootstrap的做法[url=http://getbootstrap.com/javascript/#buttons]http://getbootstrap.com/javascript/#buttons[/url]
[b]这种方式只能防止“多次点击提交按钮”,对于其他的场景不适用![/b]
(2)不disable按钮,通过全局变量来控制多次点击(只有页面重新加载后可以再次点击)。
需要注意的是如果是AJAX请求的话一定要在请求回来后重置这个值:
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,需要自己通过代码实现:
参考:
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
[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