二重送信防止

Springから提供されている機能だけでは二重送信を防ぐことはできません。 ここでは keel-spring-enhance二重送信防止機能 を使用して二重送信を防ぐ方法について説明します。

以下のサンプルコードの動作確認環境については、 動作確認環境と依存ライブラリについて を参照してください。

設定例

pom.xml

依存ライブラリにkeel-spring-boot-starter-webを追加します。

<dependency>
  <groupId>jp.fintan.keel</groupId>
  <artifactId>keel-spring-boot-starter-web</artifactId>
  <version>2.0.0</version>
</dependency>

実装例

Controller

サンプルアプリケーションでは、入力画面⇒確認画面⇒登録完了画面の流れで遷移します。トークンの生成やチェックは、以下のタイミングで実施しています。

  • 確認画面への遷移リクエスト

    • @TransactionTokenCheck(type = TransactionTokenType.BEGIN) を使用してトークンを生成します。

      • 確認画面のhidden項目にトークンが自動設定されます。

      • サーバ側ではトークンが保持されます。

  • 登録処理へのリクエスト

    • @TransactionTokenCheck を使用して、リクエストで送信されるトークンとサーバ側に保持したトークンをチェックします。

      • トークンチェックをパスした場合は、入力情報をデータベースに登録して登録完了画面にリダイレクトします。

      • トークンチェックエラーの場合は、エラー画面を返却します。

@Controller
@RequestMapping("user")
// 生成するトークンのネームスペースを設定します。
@TransactionTokenCheck("user")
public class UserController {

    private final Logger logger = LoggerFactory.getLogger(UserController.class);

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @ModelAttribute
    public UserForm userForm() {
        return new UserForm();
    }

    @GetMapping
    public String input() {
        return "user/input";
    }

    // @TransactionTokenCheckのtype属性にTransactionTokenType.BEGINを設定して、トークンを生成します。
    @TransactionTokenCheck(type = TransactionTokenType.BEGIN)
    @PostMapping("/confirm")
    public String confirm(@Validated UserForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/input";
        }
        return "user/confirm";
    }

    // @TransactionTokenCheck設定して、リクエストで送信されるトークンとサーバで保持しているトークンが同一であるかをチェックします。
    // 同一でない場合は、InvalidTransactionTokenExceptionが送出されます。
    // @TransactionTokenCheckのtype属性のデフォルト値は、TransactionTokenType.INです。
    @TransactionTokenCheck
    @PostMapping("/create")
    public String create(@Validated UserForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/input";
        }

        userService.insert(new User(form.getName(), form.getAge()));

        // 登録完了後の画面でページの再読込みを実施した場合に登録処理が再実行されることを防止するため、
        // PRG(Post-Redirect-Get)パターンを適用しています。
        return "redirect:/user/complete";
    }

    @GetMapping("/complete")
    public String complete() {
        return "user/complete";
    }

    // @ExceptionHandlerを使用して、InvalidTransactionTokenExceptionが送出された場合のハンドリングをします。
    @ExceptionHandler(InvalidTransactionTokenException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public String invalidTransactionTokenExceptionHandler(InvalidTransactionTokenException e) {
        if (logger.isDebugEnabled()) {
            logger.debug(e.getMessage());
        }
        return "error/token-error";
    }
}

サンプル全体は double-submission を参照してください。