1. 程式人生 > >Spring MVC攔截器+註解方式實現防止表單重複提交

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定義的元註解:

[email protected]

[email protected]

3.Documented

[email protected]

@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:在執行時有效(即執行時保留)

@Inherited

@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}">

這樣,防止重複提交的問題就解決了!謝謝各位的支援,期待交流!