springmvc 防止表單重複提交
在網上查看了很多關於springmvc防止表單重複提交的例子,將其中的3個整合在一起,才算是一個完整的例子。因專案中暫時還沒有這麼個需求,特備註下來,以防以後用到時還要大量的漫無目的的去尋找例子。
實現原理為SpringMvc攔截器+註解的方式實現防止重複提交
實現機制是使用token(隨機碼),簡單說下:
(a)進入下單頁,會生成一個token,同時存在兩個地方:session(或redis也可以)和頁面
(b)提交時,伺服器接收到頁面的token後,會和session中的token比較,相同則允許提交,同時刪除session中的token;
(c)如果重複提交,則session中已經沒有token(已被步驟b刪除),那麼校驗不通過,則不會真正提交.
先建立一個註解:
package com.dinfo.interceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
boolean save() default false;
boolean remove() default false;
}
攔截器介面:
package com.dinfo.interceptor;
import java.lang.reflect.Method;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class TokenInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOG = Logger.getLogger(Token.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();
Token annotation = method.getAnnotation(Token.class);
if (annotation != null) {
boolean needSaveSession = annotation.save();
if (needSaveSession) {
request.getSession(true).setAttribute("token", UUID.randomUUID().toString());
}
boolean needRemoveSession = annotation.remove();
if (needRemoveSession) {
if (isRepeatSubmit(request)) {
LOG.warn("please don't repeat submit,url:"+ request.getServletPath());
return false;
}
request.getSession(true).removeAttribute("token");
}
}
return true;
} else {
return super.preHandle(request, response, handler);
}
}
private boolean isRepeatSubmit(HttpServletRequest request) {
String serverToken = (String) request.getSession(true).getAttribute("token");
if (serverToken == null) {
return true;
}
String clinetToken = request.getParameter("token");
if (clinetToken == null) {
return true;
}
if (!serverToken.equals(clinetToken)) {
return true;
}
return false;
}
}
配置檔案如下:
<!-- 攔截器配置 -->
<mvc:interceptors>
<!-- 配置Token攔截器,防止使用者重複提交資料 -->
<mvc:interceptor>
<mvc:mapping path="/**"/><!--這個地方時你要攔截得路徑 我這個意思是攔截所有得URL-->
<bean class="com.dinfo.interceptor.TokenInterceptor"/><!--class檔案路徑改成你自己寫得攔截器路徑!! -->
</mvc:interceptor>
</mvc:interceptors>
頁面表單中需要新增如下:<input type="hidden" name="token" value="${token}" />
在需要生成token(通常是要點選提交得那個頁面)得Controller中使用我們剛才自定義好得註解,貼程式碼:
@SuppressWarnings({ "unchecked", "finally", "rawtypes" })
@RequestMapping("/SaveDataController/show")
@Token(save=true)
public String saveData(HttpServletRequest request,HttpServletResponse response,String task_id){
Map map = new HashMap();
System.out.println(task_id);
try {
map = saveDataService.queryByTaskId(task_id);
} catch (Exception e) {
e.printStackTrace();
map.put("task_id", task_id);
map.put("savetype", "");
map.put("memoryCodes", "1");
map.put("tablename", "");
map.put("trowkey", "");
map.put("columns", "");
map.put("indextablename", "");
map.put("irowkey", "");
map.put("icolumns", "");
} finally {
request.setAttribute("map", map);
return "savedata/index";
}
}
只要通過這個方法跳向得頁面都是隨機生成一個token,然後再真正再提交觸發得方法上加入@token(remove=true),貼程式碼:
@RequestMapping("/SaveDataController/saveData")
@ResponseBody
@Token(remove=true)
public void saveData(HttpServletRequest request,HttpServletResponse response,
String tablename,String trowkey,String columns,
String indextablename,String irowkey,String icolumns,
String task_id,String savetype,String memoryCodes){
System.out.println(task_id);
saveDataService.saveData(task_id,savetype,memoryCodes,tablename, trowkey, columns, indextablename, irowkey, icolumns);
}
上面的只能防止單頁面的重複提交,如果同時開啟兩個頁面同時進行提交,則第二次提交總是失敗,需要進行再次擴充套件,具體方法如下:
但是--
如果開啟兩個標籤頁,則這兩個頁面分別有一個token,並且是不同的(理論上是不同的),但是session中只有一份,
其中一個提交後,就會刪除session中的token,那麼另外一個頁面提交時session中的token已為空,那麼一定校驗不通過.
根本原因:
在同一時刻,session中只存了一份token,而頁面上可能有多個token(有n個頁籤,就會有n個不同的token).
既然找到了根本原因,那麼也就自然而然產生了解決方案.
思路:session中不能只儲存一份token,而是支援儲存多個token
具體技術方案:
(1)每次進入下單頁都會產生一個新的token,這個token都進入兩個資料流:
--(a)傳到頁面,key是repeattoken
前端頁面引用方式:
Html程式碼- <input type="hidden" name="repeattoken" value="${Session.repeattoken!}" >
--(b)儲存到session(現在是增量,不會把原來的token替換掉)
(2)儲存到session中的key應該與頁面的key區分開來,使用"tokenpool"
優化之後的過濾器:
Java程式碼- 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;
- /**
- * 防止下單頁重複提交
- */
- 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;
- }
- }