【Springboot】フォームからのコメントの重複送信防止(前後分離&単一ノード)


記事の目次
  • クライテリア
  • 装備
  • Core-Code
  • 新規注記@AvoidDuplicateFormToken
  • 異常処理
  • キャッシュクラス
  • カスタムフォームブロッカー
  • SpringBoot構成ブロッキング
  • Controller使用
  • 総括
  • 注意点
  • 個人推奨
  • github
  • 著者
  • 前言
    最近、マイクロサービスとweb関連を更新しました.ビッグデータ補完
    SpringBootはフォームの重複コミットを防止します.ブロッキングに基づいて注釈付きリクエストをブロックし,処理する.
    なぜこのように使うのか、後でまとめます.
    適用シーン:
  • ブラウザの戻るボタンを使用して、以前の操作を繰り返し、フォームを繰り返しコミットします.重要なビジネスでは、最も一般的な注文シーンなど、大きな問題が発生します.次の2つのリストでは、計算された金額が異なります.
  • 私たちのプログラムはそんなに忙しくても、重複したHTTPリクエストを処理する必要はありません.

  • 注意:
  • 単一ノード(マルチノードの非適用)
  • 前後端分離(前後端が分離しない方が簡単.後述)
  • に装備を施す
  • SpringBoot 2.0.3

  • Core-Code
    新規注記@AvoidDuplicateFormToken
    /**
     * AvoidDuplicateSubmit
     * 
     * Description:           
     * 
     * Creation Time: 2018/11/28 19:27.
     *
     * @author Hu weihui
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AvoidDuplicateFormToken {
    
    }
    
    異常処理
    /**
     * FormTokenException
     * 
     * Description:        
     * 
     * Creation Time: 2018/12/3 15:26.
     *
     * @author Hu weihui
     */
    public class FormTokenException extends RuntimeException{
        private static final long serialVersionUID = 512936007428810210L;
    
        private String errorCode;
    
        private String errorMsg;
    
        public FormTokenException(String errorCode,String errorMsg) {
            super(errorMsg);
            this.errorCode = errorCode;
        }
    
    
        public FormTokenException(String errorCode,String errorMsg,Throwable cause) {
            super(errorMsg,cause);
            this.errorCode = errorCode;
        }
    
        public FormTokenException(FormTokenExceptionEnum formTokenExceptionEnum) {
            super(formTokenExceptionEnum.getErrorMsg());
            this.errorCode = formTokenExceptionEnum.getErrorCode();
        }
    
        public FormTokenException(FormTokenExceptionEnum formTokenExceptionEnum,Throwable cause) {
            super(formTokenExceptionEnum.getErrorMsg(),cause);
            this.errorCode = errorCode;
        }
    
    /**
     * FormExceptionEnum
     * 
     * Description:            
     * 
     * Creation Time: 2018/11/29 14:15.
     *
     * @author Hu weihui
     */
    @Getter
    public enum FormTokenExceptionEnum {
        DUPLICATE_SUBMIT("FT-001", ErrorConstant.NETWORK_ERROR, "      "),
    
        ILLEGAL_SUBMIT("FT-002",ErrorConstant.NETWORK_ERROR,"      "),
    
        SERVER_TOKEN_ERROR("FT-003",ErrorConstant.NETWORK_ERROR,"         "),
    
        UNKONW_ERROR("FT-004", ErrorConstant.NETWORK_ERROR, "        ");
    
    
        private String errorCode;
    
        private String errorType;
    
        private String errorMsg;
    
        FormTokenExceptionEnum(String errorCode, String errorType, String errorMsg) {
            this.errorCode = errorCode;
            this.errorType = errorType;
            this.errorMsg = errorMsg;
        }
    }
    
    /**
     * ErrorConstant
     * 
     * Description:     
     * 
     * Creation Time: 2018/12/3 15:28.
     *
     * @author Hu weihui
     */
    public class ErrorConstant {
        public static final String SYSTEM_ERROR = "    ";
    
        public static final String UNKNOW_ERROR = "    ";
    
        public static final String NETWORK_ERROR = "    ";
    
        public static final String BUSINESS_ERROR = "    ";
    
        public static final String VALID_ERROR = "      ";
    }
    
    キャッシュクラス
    /**
     * UserCache
     * 
     * Description:
     * 
     * Creation Time: 2018/12/3 11:00.
     *
     * @author Hu weihui
     */
    public class UserCache {
        /**
         *       cache,   2 .
         *
         * @return the cache
         * @author : Hu weihui
         */
        @Bean
        public Cache<String,String> getUserCache(){
            return CacheBuilder.newBuilder().expireAfterAccess(2L,TimeUnit.SECONDS).build();
        }
    }
    
    カスタムフォームブロッカー
  • の場合一:単一ノードアプリケーション
  • の場合2:前後端分離=>このときフロントエンドは一般的に要求ヘッダを介して検査済みのUserToken
  • に伝達される.
  • ケース3:前後端非分離=>userIdにより取得したユーザ情報をsessionに保存して検証する
  • 以下の場合は前後端分離です.
    前後が離れないのは簡単です.request.getSession()は後続操作をすればOK
    /**
     * DuplicateSubmitInterceptor
     * 
     * Description:          (   ,       )
     *                 ->       USER_TOKEN
     *                  ->       Session
     * 
     * Creation Time: 2018/12/3 14:25.
     *
     * @author Hu weihui
     */
    @Slf4j
    public class DuplicateSubmitInterceptor extends HandlerInterceptorAdapter {
    
        private static final String USER_TOKEN_KEY = "token";
    
        @Autowired
        private Cache<String, String> cache;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
     		if (handler instanceof ResourceHttpRequestHandler) {
                return true;
            }
            
            HandlerMethod handlerMethod = (HandlerMethod) handler;
    
            Method method = handlerMethod.getMethod();
    
            AvoidDuplicateFormToken annotation = method.getAnnotation(AvoidDuplicateFormToken.class);
    		//       
            if (annotation != null) {
                boolean result = !isDuplicateSubmit(request);
                return result;
            }
            return super.preHandle(request, response, handler);
        }
    
    
        /**
         *           .
         *
         * @param request the request
         * @return the boolean
         * @author : Hu weihui
         */
        private boolean isDuplicateSubmit(HttpServletRequest request) {
            try {
                //      token,        
                String userToken = request.getHeader(USER_TOKEN_KEY);
    
                if (StringUtils.isEmpty(userToken)) {
    
                    throw new FormTokenException(FormTokenExceptionEnum.ILLEGAL_SUBMIT);
    
                }
    
                String clientoken = cache.getIfPresent(userToken);
                //  cache    token,token2    ,       
                if (null != clientoken){
                    log.info("      :  token: {},  token: {}", userToken);
                    throw new FormTokenException(FormTokenExceptionEnum.DUPLICATE_SUBMIT);
                }else {
                    //  token     /    ,   cache
                    cache.put(userToken,UUID.randomUUID().toString());
                }
    
            } catch (Exception e) {
    
                log.info("           ,{}", e.getMessage());
    
                throw new FormTokenException(FormTokenExceptionEnum.SERVER_TOKEN_ERROR);
    
            }
    
            return false;
        }
    }
    
    
    SpringBootブロッキングの構成
    /**
     * WebConfig
     * 
     * Description:
     * 
     * Creation Time: 2018/12/3 15:31.
     *
     * @author Hu weihui
     */
    public class WebConfig implements WebMvcConfigurer {
    	//     
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new DuplicateSubmitInterceptor())
                    .addPathPatterns("/**")
                    .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
        }
    }
    
    コントローラ使用
    	@AvoidDuplicateFormToken
        @GetMapping("/test")
        public ResponseEntity<?> test() {
            return null;
        }
    
    締め括りをつける
    注意点
  • SpringBoot 2.xはimplements WebMvcConfigurer{}実装ブロッキング機能
  • を使用する
  • 【DuplicateSubmitInterceptor】 HandlerMethod handlerMethod = (HandlerMethod) handler;java.lang.ClassCastException:org.springframework.web.servlet.resource.D e f a l t v e r t H t p RequestHandler cannot be cast to org.springframework.web.method.Handler Method.ここで明らかにswaggerの静的リソースマッチング要求の問題に起因する.この方法は静的リソースの処理としてデフォルトで使用されるため、.excludePathPatterns("/swagger-resources/"、/webjars/"、/v 2/"、"/swagger-ui.html/")を除外する必要があります.ここで参考にして、この友达のソース分析、とても感謝します:https://yq.aliyun.com/articles/515182
  • なぜhashmapでストレージができないのかという友人もいます.ここで私はいつremoveしますか?制御できません.最良の実施形態は、echacheを使用してexpireTimeタイムアウト時間を構成することです.
  • このスキームは単一ノードである.分布式の場合はredis、弱いもの、databaseなどを使ってもいいですが、tokenを記録することに重点を置いています.

  • 個人的なアドバイス
  • フォームの重複コミットがビジネスに基づいて行われることを防止します.各システムにこの機能がある必要はありません.最も古典的な場面はショッピングカートが注文を提出することです.実際のビジネスこそ、カスタマイズされた機能とアーキテクチャのベンチマーク
  • です.
  • フォームを使用して繰り返しコミットするかどうかにかかわらず、データベースは一意の制約を行い、一般的な繰り返しコミットの問題を解決します.

  • github
    https://github.com/ithuhui/hui-base-java
    【hui-base-common】の下のcom.hui.base.comon.interceptor
    作成者
       :HuHui
       :      web      ,            ,