Spring MVC服务器端防止重复提交
参考:http://blog.csdn.net/hw1287789687/article/details/51732373
之前参考 http://zhengyunfei.iteye.com/blog/2307443实现了功能
但是测试同学发现,打开两个待提交的页签时,提交其中一个一定会报错:
实现机制是使用token,简单说下:
(a)进入下单页,会生成一个token,同时存在两个地方:session(或redis也可以)和页面
(b)提交时,服务器接收到页面的token后,会和session中的token比较,相同则允许提交,同时删除session中的token;
(c)如果重复提交,则session中已经没有token(已被步骤b删除),那么校验不通过,则不会真正提交.
拦截器代码:
package com.chanjet.gov.filter;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* Created by 黄威 on 9/20/16.<br >
* 防止下单页重复提交
*/
public class RepeatTokenInterceptor extends HandlerInterceptorAdapter {
private static Logger log = Logger.getLogger(RepeatTokenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmitToken annotation = method.getAnnotation(RepeatSubmitToken.class);
if (annotation != null) {
boolean needSaveSession = annotation.save();
if (needSaveSession) {
request.getSession(true).setAttribute("repeattoken", UUID.randomUUID().toString());
}
boolean needRemoveSession = annotation.remove();
if (needRemoveSession) {
if (isRepeatSubmit(request)) {
log.warn("please don't repeat submit,url:" + request.getServletPath());
response.sendRedirect("/warn.html?code=repeatSubmitOrder&orgId="+request.getParameter("orgId"));
return false;
}
request.getSession(true).removeAttribute("repeattoken");
}
}
return true;
} else {
return super.preHandle(request, response, handler);
}
}
private boolean isRepeatSubmit(HttpServletRequest request) {
String serverToken = (String) request.getSession(true).getAttribute("repeattoken");
if (serverToken == null) {
return true;
}
String clinetToken = request.getParameter("repeattoken");
if (clinetToken == null) {
return true;
}
if (!serverToken.equals(clinetToken)) {
return true;
}
return false;
}
}
但是--
如果打开两个标签页,则这两个页面分别有一个token,并且是不同的(理论上是不同的),但是session中只有一份,
其中一个提交后,就会删除session中的token,那么另外一个页面提交时session中的token已为空,那么一定校验不通过.
根本原因:
在同一时刻,session中只存了一份token,而页面上可能有多个token(有n个页签,就会有n个不同的token).
既然找到了根本原因,那么也就自然而然产生了解决方案.
思路:session中不能只存储一份token,而是支持存储多个token
具体技术方案:
(1)每次进入下单页都会产生一个新的token,这个token都进入两个数据流:
--(a)传到页面,key是repeattoken
前端页面引用方式:
<input type="hidden" name="repeattoken" value="${Session.repeattoken!}" >
--(b)存储到session(现在是增量,不会把原来的token替换掉)
(2)存储到session中的key应该与页面的key区分开来,使用"tokenpool"
优化之后的过滤器:
package com.chanjet.gov.filter;
import com.chanjet.gov.util.StringUtil;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* Created by 黄威 on 9/20/16.<br >
* 防止下单页重复提交
*/
public class RepeatTokenInterceptor extends HandlerInterceptorAdapter {
/***
* 用于前端页面接收服务器端的token
*/
public static final String SESSION_KEY_REPEATTOKEN="repeattoken";
/***
* 用于session存储n个token
*/
public static final String SESSION_KEY_REPEATTOKEN_POOL = "tokenpool";
private static Logger log = Logger.getLogger(RepeatTokenInterceptor.class);
private void addRepeatToken(HttpServletRequest request, HttpServletResponse response){
HttpSession httpSession = request.getSession(true);
String token = (String) httpSession.getAttribute(SESSION_KEY_REPEATTOKEN_POOL);
System.out.println("addRepeatToken token:"+token);
String createdToken = UUID.randomUUID().toString();
httpSession.setAttribute(SESSION_KEY_REPEATTOKEN, createdToken);//给前端页面用的
if(StringUtil.isNullOrEmpty(token)){
httpSession.setAttribute(SESSION_KEY_REPEATTOKEN_POOL, createdToken);
}else{
httpSession.setAttribute(SESSION_KEY_REPEATTOKEN_POOL, createdToken + "###" + token);
}
}
private void removeRepeatToken(HttpServletRequest request, HttpServletResponse response){
HttpSession httpSession = request.getSession(true);
String token = (String) httpSession.getAttribute(SESSION_KEY_REPEATTOKEN_POOL);
System.out.println("removeRepeatToken token:"+token);
if(!StringUtil.isNullOrEmpty(token)){
String clientToken = (String) request.getParameter(SESSION_KEY_REPEATTOKEN);
System.out.println("removeRepeatToken serverToken:"+clientToken);
if (clientToken == null) {
return;
}
token = token.replace(clientToken, "").replace("######", "###");
System.out.println("token:"+token);
httpSession.setAttribute(SESSION_KEY_REPEATTOKEN_POOL, token);
}
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmitToken annotation = method.getAnnotation(RepeatSubmitToken.class);
if (annotation != null) {
boolean needSaveSession = annotation.save();
if (needSaveSession) {
addRepeatToken(request,response);
}
boolean needRemoveSession = annotation.remove();
if (needRemoveSession) {
if (isRepeatSubmit(request)) {
log.warn("please don't repeat submit,url:" + request.getServletPath());
response.sendRedirect("/warn.html?code=repeatSubmitOrder&orgId="+request.getParameter("orgId"));
return false;
}
removeRepeatToken(request,response);
}
}
return true;
} else {
return super.preHandle(request, response, handler);
}
}
private boolean isRepeatSubmit(HttpServletRequest request) {
//从池子里面获取token
String serverToken = (String) request.getSession(true).getAttribute(SESSION_KEY_REPEATTOKEN_POOL);
if (serverToken == null||"###".equals(serverToken)) {
return true;
}
String clinetToken = request.getParameter(SESSION_KEY_REPEATTOKEN);//请求要素
if (StringUtil.isNullOrEmpty(clinetToken)) {
return true;
}
System.out.println("clinetToken:"+clinetToken);
if (!serverToken.contains(clinetToken)) {
return true;
}
return false;
}
}
日志:
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
removeRepeatToken token:c171f626-f218-4d19-bbb8-7aa209523c69###1b50afdf-df24-4553-89d2-701af06a431e
removeRepeatToken serverToken:1b50afdf-df24-4553-89d2-701af06a431e
token:c171f626-f218-4d19-bbb8-7aa209523c69###
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:03,379 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:03,546 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:03,751 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:03,919 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:04,129 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:04,370 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:04,476 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:04,703 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
clinetToken:1b50afdf-df24-4553-89d2-701af06a431e
09:26:04,874 WARN RepeatTokenInterceptor:70 - please don't repeat submit,url:/order/submitOrder
submit