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 ;
    }
}

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM