Spring MVC攔截器+註解方式實現防止表單重複提交
表單重複提交是在多使用者Web應用中最常見、帶來很多麻煩的一個問題。有很多的應用場景都會遇到重複提交問題,比如:
1.點選提交按鈕兩次。
2.點選重新整理按鈕。
3.使用瀏覽器後退按鈕重複之前的操作,導致重複提交表單。
4.使用瀏覽器歷史記錄重複提交表單。
5.瀏覽器重複的HTTP請求。
當然,解決該問題的方法不止一種,但是我這裡推薦我使用的方法:攔截器+註解方式
基本的原理:
url請求時,用攔截器攔截,生成一個唯一的識別符號(token),在新建頁面中Session儲存token隨機碼,當儲存時驗證,通過後刪除,當再次點選儲存時由於伺服器端的Session中已經不存在了,所有無法驗證通過。
一、自定義註解
package com.pengtu.gsj.interceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>
* 防止重複提交註解,用於方法上<br/>
* 在新建頁面方法上,設定save為true,此時攔截器會在Session中儲存一個token,
* 同時需要在新建的頁面中新增
* <input type="hidden" name="token" value="${token}">
* <br/>
* 儲存方法需要驗證重複提交的,設定remove為true
* 此時會在攔截器中驗證是否重複提交
* </p>
* @author: zl
* @date: 2017-4-24下午4:24:02
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
boolean save() default false;
boolean remove() default false;
}
可能對自定義註解不是很瞭解,下面做一個說明:
首先要知道幾個元註解:
元註解的作用就是負責註解其他註解。Java5.0定義了4個標準的meta-annotation型別,它們被用來提供對其它 annotation型別作說明。Java5.0定義的元註解:
3.Documented
@Target:
作用:用於描述註解的使用範圍(即:被描述的註解可以用在什麼地方)
取值(ElementType)有:
1.CONSTRUCTOR:用於描述構造器
2.FIELD:用於描述域
3.LOCAL_VARIABLE:用於描述區域性變數
4.METHOD:用於描述方法
5.PACKAGE:用於描述包
6.PARAMETER:用於描述引數
7.TYPE:用於描述類、介面(包括註解型別) 或enum宣告
@Retention:
作用:表示需要在什麼級別儲存該註釋資訊,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)
取值(RetentionPoicy)有:
1.SOURCE:在原始檔中有效(即原始檔保留)
2.CLASS:在class檔案中有效(即class保留)
3.RUNTIME:在執行時有效(即執行時保留)
@interface自定義註解時,自動繼承了java.lang.annotation.Annotation介面,由編譯程式自動完成其他細節。在定義註解時,不能繼承其他的註解或介面。@interface用來宣告一個註解,其中的每一個方法實際上是聲明瞭一個配置引數。方法的名稱就是引數的名稱,返回值型別就是引數的型別(返回值型別只能是基本型別、Class、String、enum)。可以通過default來宣告引數的預設值。
定義註解格式:
public @interface 註解名 {定義體}
Annotation型別裡面的引數該怎麼設定:
第一,只能用public或預設(default)這兩個訪問權修飾.例如,String value();這裡把方法設為defaul預設型別;
第二,引數成員只能用基本型別byte,short,char,int,long,float,double,boolean八種基本資料型別和 String,Enum,Class,annotations等資料型別,以及這一些型別的陣列.例如,String value();這裡的引數成員就為String;
第三,如果只有一個引數成員,最好把引數名稱設為"value",後加小括號.例:下面的例子FruitName註解就只有一個引數成員。
二、新建攔截器
方法語句很簡單,就沒有添加註解。
package com.pengtu.gsj.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;
import com.pengtu.gsj.entity.app.User;
import com.pengtu.gsj.utils.UserUtils;
import com.pengtu.gsj.utils.web.SpringMvcUtils;
public class AvoidDuplicateSubmissionInterceptor extends HandlerInterceptorAdapter {
public static Logger logger = Logger.getLogger(AvoidDuplicateSubmissionInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
User user = UserUtils.getUser();
if (user != null) {
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)) {
logger.warn("please don't repeat submit,[user:" + user.getUsername() + ",url:"
+ request.getServletPath() + "]");
return false;
}
request.getSession(false).removeAttribute("token");
}
}
}
return true;
}
public boolean isRepeatSubmit (HttpServletRequest request) {
String serverToken = (String) request.getSession(true).getAttribute("token");
if (serverToken == null) {
return true;
}
String clientToken = SpringMvcUtils.getParameter("token");
if (clientToken == null) {
return true;
}
if (!serverToken.equals(clientToken)) {
return true;
}
return false;
}
}
三、在springmvc中配置攔截器:
<!-- 攔截器配置 -->
<mvc:interceptors>
<!-- 配置Token攔截器,防止使用者重複提交資料 -->
<mvc:interceptor>
<mvc:mapping path="/**" /><!--這個地方時你要攔截得路徑 我是攔截所有得URL -->
<bean class="com.pengtu.gsj.interceptor.AvoidDuplicateSubmissionInterceptor" /><!--class檔案路徑為攔截器路徑!! -->
</mvc:interceptor>
</mvc:interceptors>
當然也可也在spring.xml中配置bean,我選擇的是前者
四、在相關方法中加入註解:比如跳轉到新增(檢視)資訊頁面的方法前面需要新增@Token(save=true) 生成token,儲存在頁面中;在儲存的方法前面新增@token(remove=true),檢查session是否存在,存在即通過並刪除token值
@RequestMapping("input")
@Token(save = true)
public String showOrInputUserInfo(@ModelAttribute User user,Model model) {
List<Role> allRoles = roleService.getAllRole();
model.addAttribute("allRoles", allRoles);
return "system/user_input";
}
@RequestMapping("savePerson")
@Token(remove = true)
public String savePersonInfo(@ModelAttribute User user, RedirectAttributes attributes) {
userService.saveUser(user);
UserUtils.putCache(UserUtils.CACHE_USER, user); //更新快取裡面當前使用者資訊
attributes.addFlashAttribute("msg", "資訊更新成功!");
attributes.addAttribute("top", SpringMvcUtils.getParameter("top"));
attributes.addAttribute("left", SpringMvcUtils.getParameter("left"));
return "redirect:/user/view.do";
}
五、在新建頁面中加入token
<input type="hidden" name="token" value="${token}">
這樣,防止重複提交的問題就解決了!謝謝各位的支援,期待交流!