spring boot 學習(七)小工具篇:表單重複提交
註解 + 攔截器:解決表單重複提交
前言
學習 Spring Boot 中,我想將我在專案中新增幾個我在 SpringMVC 框架中常用的工具類(主要都是涉及到 Spring AOP 部分知識)。比如,表單重複提交,?秒防重新整理,全域性異常捕抓類,IP黑名單(防爬蟲設定)…………等等。接下來的時間,我嘗試將這些框架整合到 Spring Boot 中(儘可能完成),畢竟專案開發中這些工具是非常有用的。
注意,這些工具基本上都是我以前在 github 之類開源平臺找到的小工具類,作者的資訊什麼的許多都忘了。先說聲不好意思了。若有相關資訊,麻煩提醒一下~
介紹
這裡就不詳細介紹相應的知識了,主要提及有關涉及到的術語:
攔截器
Spring 攔截器有兩種實現方法。一種是繼承HandlerInterceptorAdapter
,擁有preHandle
(業務處理器處理請求之前被呼叫),postHandle
(在業務處理器處理請求執行完成後,生成檢視之前執行),afterCompletion
(在完全處理完請求後被呼叫,可用於清理資源等)三個方法。
另一種就是呼叫 Spring AOP 的方法來實現。而且,我覺得這種方法更加靈活方便,所以我比較經常使用這種方法。AOP( AspectJ— 註解 風格)
AOP 就是 Aspect Oriented Programming(面向方面程式設計)。
1. 連線點(Joinpoint)
2. 前置通知(@Before):在某連線點(join point)之前執行的通知,但這個通知不能阻止連線點前的執行(除非它丟擲一個異常)。
3. 丟擲異常後通知(@AfterThrowing):方法丟擲異常退出時執行的通知
附上:大神開濤的有關 Spring AOP 部落格:http://jinnianshilongnian.iteye.com/blog/1474325
解決問題
什麼是表單重複提交?
伺服器認為是同一個表單,在短時間內重複(不止一次)提交,或者提交異常。比如,在伺服器還沒有響應前我們不斷點選重新整理網頁上一個提交按鈕,或者通過 ajax 不斷對伺服器傳送請求報文!
防止情況
- 不通過正常路徑訪問頁面表單;
- session 失效情況下提交表單;
- 短時間內不止一次提交表單。
解決方案
一般情況下,是在伺服器利用 session 來防止這個問題的。
流程圖:
1. 網頁點選事件,網頁提交發送申請;
2. 伺服器收到申請,併產生令牌(Token),並存於 Session 中;
3. 伺服器將令牌返回給頁面,頁面將令牌與表單真正提交給伺服器。
這種就是 structs 的令牌方式。還有其他方法,就是重定向方法或設定頁面過期(前端部分不太瞭解),不過還是感覺強制跳轉不是特別友好,同時也不夠靈活多用。
前期準備
新建一個 spring boot 專案(建議 1.3.X 以上版本)。
加入 aop 依賴,預設設定就行了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
正式開工
- 註解類 Token.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Token {
//生成 Token 標誌
boolean save() default false ;
//移除 Token 值
boolean remove() default false ;
}
- 表單異常類 FormRepeatException.java
public class FormRepeatException extends RuntimeException {
public FormRepeatException(String message){ super(message);}
public FormRepeatException(String message, Throwable cause){ super(message, cause);}
}
- 攔截器 TokenContract.java
注意:@Aspect
與@Component
兩個註解!
@Aspect
@Component
public class TokenContract {
private static final Logger logger = LoggerFactory.getLogger(TokenContract.class);
@Before("within(@org.springframework.stereotype.Controller *) && @annotation(token)")
public void testToken(final JoinPoint joinPoint, Token token){
try {
if (token != null) {
//獲取 joinPoint 的全部引數
Object[] args = joinPoint.getArgs();
HttpServletRequest request = null;
HttpServletResponse response = null;
for (int i = 0; i < args.length; i++) {
//獲得引數中的 request && response
if (args[i] instanceof HttpServletRequest) {
request = (HttpServletRequest) args[i];
}
if (args[i] instanceof HttpServletResponse) {
response = (HttpServletResponse) args[i];
}
}
boolean needSaveSession = token.save();
if (needSaveSession){
String uuid = UUID.randomUUID().toString();
request.getSession().setAttribute( "token" , uuid);
logger.debug("進入表單頁面,Token值為:"+uuid);
}
boolean needRemoveSession = token.remove();
if (needRemoveSession) {
if (isRepeatSubmit(request)) {
logger.error("表單重複提交");
throw new FormRepeatException("表單重複提交");
}
request.getSession(false).removeAttribute( "token" );
}
}
} catch (FormRepeatException e){
throw e;
} catch (Exception e){
logger.error("token 發生異常 : "+e);
}
}
private boolean isRepeatSubmit(HttpServletRequest request) throws FormRepeatException {
String serverToken = (String) request.getSession( false ).getAttribute( "token" );
if (serverToken == null ) {
//throw new FormRepeatException("session 為空");
return true;
}
String clinetToken = request.getParameter( "token" );
if (clinetToken == null || clinetToken.equals("")) {
//throw new FormRepeatException("請從正常頁面進入!");
return true;
}
if (!serverToken.equals(clinetToken)) {
//throw new FormRepeatException("重複表單提交!");
return true ;
}
logger.debug("校驗是否重複提交:表單頁面Token值為:"+clinetToken + ",Session中的Token值為:"+serverToken);
return false ;
}
}
@Token(save = true)
@RequestMapping("/savetoken")
@ResponseBody
public String getToken(HttpServletRequest request, HttpServletResponse response){
return (String) request.getSession().getAttribute("token");
}
@Token(remove = true)
@RequestMapping("/removetoken")
@ResponseBody
public String removeToken(HttpServletRequest request, HttpServletResponse response){
return "success";
}