1. 程式人生 > >spring boot 學習(七)小工具篇:表單重複提交

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)

    :表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、方法呼叫、欄位呼叫或處理異常等等,Spring只支援方法執行連線點
    2. 前置通知(@Before):在某連線點(join point)之前執行的通知,但這個通知不能阻止連線點前的執行(除非它丟擲一個異常)。
    3. 丟擲異常後通知(@AfterThrowing):方法丟擲異常退出時執行的通知
    附上:大神開濤的有關 Spring AOP 部落格:http://jinnianshilongnian.iteye.com/blog/1474325

解決問題

什麼是表單重複提交?

伺服器認為是同一個表單,在短時間內重複(不止一次)提交,或者提交異常。比如,在伺服器還沒有響應前我們不斷點選重新整理網頁上一個提交按鈕,或者通過 ajax 不斷對伺服器傳送請求報文!

防止情況

  1. 不通過正常路徑訪問頁面表單;
  2. session 失效情況下提交表單;
  3. 短時間內不止一次提交表單。

解決方案

一般情況下,是在伺服器利用 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";
    }

專案參考地址