注解 + 攔截器:解決表單重復提交
前言
學習 Spring Boot 中,我想將我在項目中添加幾個我在 SpringMVC 框架中常用的工具類(主要都是涉及到 Spring AOP 部分知識)。比如,表單重復提交,?秒防刷新,全局異常捕抓類,IP黑名單(防爬蟲設置)…………等等。接下來的時間,我嘗試將這些框架整合到 Spring Boot 中(盡可能完成),畢竟項目開發中這些工具是非常有用的。
注意,這些工具基本上都是我以前在 github 之類開源平台找到的小工具類,作者的信息什么的許多都忘了。先說聲不好意思了。若有相關信息,麻煩提醒一下~
介紹
這里就不詳細介紹相應的知識了,主要提及有關涉及到的術語:
-
攔截器
Spring 攔截器有兩種實現方法。一種是繼承HandlerInterceptorAdapter
,擁有preHandle
(業務處理器處理請求之前被調用),postHandle
(在業務處理器處理請求執行完成后,生成視圖之前執行),afterCompletion
(在完全處理完請求后被調用,可用於清理資源等)三個方法。
另一種就是調用 Spring AOP 的方法來實現。而且,我覺得這種方法更加靈活方便,所以我比較經常使用這種方法。 -
AOP( AspectJ— 注解 風格)
AOP 就是 Aspect Oriented Programming(面向方面編程)。
1. 連接點(Joinpoint):表示需要在程序中插入橫切關注點的擴展點,連接點可能是類初始化、方法執行、方法調用、字段調用或處理異常等等,Spring只支持方法執行連接點。
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 ; } }
Controller類
訪問 http://localhost:8080/savetoken 來獲得令牌值
訪問 http://localhost:8080/removetoken?token=XXX 來提交真正的表單
@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"; }