Spring Boot セキュリティ関連

Spring BootでPRGパターンを利用してみた

PRGパターンの「PRG」は「Post&Redirect&Get」の略で、Postした後でリダイレクトして画面表示するパターンのことをいう。PRGパターンを利用すると、ブラウザを再度読み込んだ場合に再度サブミット処理が実行されるのを防ぐことができる。

PRGパターンを利用しない場合、以下のように、登録完了後に「最新の情報に更新(F5)」ボタンを押下すると、「フォームを再送信しますか?」というダイアログが表示され、そこで「再試行」を押下すると再度登録処理が行われる。

1) PRGパターンを利用しない登録画面に遷移し、下記のように入力後、「登録」ボタンを押下
PRGパターンサンプル_1

2) 下記画面が表示され、登録データが一覧下に確認される。ここで「最新の情報に更新(F5)」ボタンを押下
PRGパターンサンプル_2

3)「フォームを再送信しますか?」というダイアログが表示されるため、「再試行」ボタンを押下
PRGパターンサンプル_3

4) 下記画面が表示され、登録処理が再度行われたことが確認できる
PRGパターンサンプル_4

他方、PRGパターンを利用した場合、以下のように、登録完了後に「最新の情報に更新(F5)」ボタンを押下すると、「フォームを再送信しますか?」というダイアログが表示されず、再度の登録処理は行われない。

5) PRGパターンを利用する登録画面に遷移し、下記のように入力後、「登録」ボタンを押下
PRGパターンサンプル_5

6) 下記画面が表示され、登録データが一覧下に確認される。また、リダイレクトして新しい画面を表示しているため、登録フォームの値は全てクリアされる。ここで「最新の情報に更新(F5)」ボタンを押下
PRGパターンサンプル_6

7) 画面表示内容が変わらず、再登録はされないことが確認される。
PRGパターンサンプル_7

今回は、このPRG(Post&Redirect&Get)パターンを利用したサンプルプログラムを作成してみたので、共有する。

前提条件

下記記事の実装が完了していること。

Spring BootでAjaxを利用してみたSpring BootのWEB画面上では、Ajax通信も行うことができる。今回は、jQueryを利用しない形で、Ajax通信を含むサンプ...

サンプルプログラムの作成

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

まず、Post方式確認用画面のHTMLファイルの内容は以下の通りで、「登録」ボタン押下時に「addDataPost」メソッドが呼ばれるようになっている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <form method="post" th:action="@{/addDataPost}" th:object="${demoForm}">
        <table border="0">
            <tr>
                <td align="left" valign="top">名前:</td>
                <td>
                    <input type="text" th:value="*{name}" th:field="*{name}" />
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">生年月日:</td>
                <td>
                    <input type="text" th:value="*{birthYear}" size="4"
                           maxlength="4" th:field="*{birthYear}" />年
                    <select th:field="*{birthMonth}">
                        <option value="">---</option>
                        <option th:each="item : *{getMonthItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>月
                    <select th:field="*{birthDay}">
                        <option value="">---</option>
                        <option th:each="item : *{getDayItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>日
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">性別:</td>
                <td>
                    <for th:each="item : *{getSexItems()}">
                        <input type="radio" name="sex" th:value="${item.key}"
                               th:text="${item.value}" th:field="*{sex}" />
                    </for>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">メモ:</td>
                <td>
                    <textarea rows="6" cols="40" th:value="*{memo}" th:field="*{memo}"></textarea>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">入力確認:</td>
                <td>
                    <input type="checkbox" name="checked" th:value="確認済" th:field="*{checked}" />
                </td>
            </tr>
        </table>
        <br/><br/>
        <input type="submit" value="登録" />
    </form>
    <br/><br/><br/>
    ★以下に検索されたデータが表示されます
    <table border="1" cellpadding="5">
        <tr>
            <th>ID</th>
            <th>名前</th>
            <th>生年月日</th>
            <th>性別</th>
            <th>メモ</th>
        </tr>
        <tr th:each="obj : ${userDataList}">
            <td th:text="${obj.id}"></td>
            <td th:text="${obj.name}"></td>
            <td th:text="|${obj.birthY}年 ${obj.birthM}月 ${obj.birthD}日|"></td>
            <td th:text="${obj.sex}"></td>
            <td th:text="${obj.memo}"></td>
        </tr>
    </table>
    <br/><br/>
</body>
</html>



次に、Post&Redirect&Get方式確認用画面のHTMLファイルの内容は以下の通りで、「登録」ボタン押下時に「addDataPostRedirectGet」メソッドが呼ばれるようになっている。それ以外はPost方式確認用画面のHTMLファイルと同じ内容になっている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <form method="post" th:action="@{/addDataPostRedirectGet}" th:object="${demoForm}">
        <table border="0">
            <tr>
                <td align="left" valign="top">名前:</td>
                <td>
                    <input type="text" th:value="*{name}" th:field="*{name}" />
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">生年月日:</td>
                <td>
                    <input type="text" th:value="*{birthYear}" size="4"
                           maxlength="4" th:field="*{birthYear}" />年
                    <select th:field="*{birthMonth}">
                        <option value="">---</option>
                        <option th:each="item : *{getMonthItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>月
                    <select th:field="*{birthDay}">
                        <option value="">---</option>
                        <option th:each="item : *{getDayItems()}"
                                th:value="${item.key}" th:text="${item.value}"/>
                    </select>日
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">性別:</td>
                <td>
                    <for th:each="item : *{getSexItems()}">
                        <input type="radio" name="sex" th:value="${item.key}"
                               th:text="${item.value}" th:field="*{sex}" />
                    </for>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">メモ:</td>
                <td>
                    <textarea rows="6" cols="40" th:value="*{memo}" th:field="*{memo}"></textarea>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">入力確認:</td>
                <td>
                    <input type="checkbox" name="checked" th:value="確認済" th:field="*{checked}" />
                </td>
            </tr>
        </table>
        <br/><br/>
        <input type="submit" value="登録" />
    </form>
    <br/><br/><br/>
    ★以下に検索されたデータが表示されます
    <table border="1" cellpadding="5">
        <tr>
            <th>ID</th>
            <th>名前</th>
            <th>生年月日</th>
            <th>性別</th>
            <th>メモ</th>
        </tr>
        <tr th:each="obj : ${userDataList}">
            <td th:text="${obj.id}"></td>
            <td th:text="${obj.name}"></td>
            <td th:text="|${obj.birthY}年 ${obj.birthM}月 ${obj.birthD}日|"></td>
            <td th:text="${obj.sex}"></td>
            <td th:text="${obj.memo}"></td>
        </tr>
    </table>
    <br/><br/>
</body>
</html>

また、初期表示画面は以下の通りで、「index_post.html」「index_prg.html」へ画面遷移する内容になっている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <form method="get" th:action="@{/index_post}">
        <input type="submit" value="Post方式確認用画面に遷移" />
    </form>
    <br/><br/>
    <form method="get" th:action="@{/index_prg}">
        <input type="submit" value="Post&Redirect&Get方式確認用画面に遷移" />
    </form>
</body>
</html>



また、コントローラクラスの内容は以下の通りで、「addDataPost」メソッドは登録後Postのみ行っていて、「addDataPostRedirectGet」メソッドはPost後にRedirectしてGetする処理を行っている。その他、「index_post.html」「index_prg.html」画面表示時は、ユーザーリストを取得している。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;

import static org.springframework.data.domain.Sort.Direction.ASC;
import static org.springframework.data.domain.Sort.Direction.DESC;

@Controller
public class DemoController {

    /**
     * ユーザーデータテーブル(user_data)へアクセスするリポジトリ
     */
    @Autowired
    private UserDataRepository repository;

    /**
     * 追加・更新用Formオブジェクトを初期化して返却する
     * @return 追加・更新用Formオブジェクト
     */
    @ModelAttribute("demoForm")
    public DemoForm createDemoForm(){
        DemoForm demoForm = new DemoForm();
        return demoForm;
    }

    /**
     * 初期表示画面に遷移
     * @return 初期表示画面
     */
    @RequestMapping("/")
    public String index(){
        return "index";
    }

    /**
     * Post方式確認用の画面に遷移する
     * @model Modelオブジェクト
     * @return Post方式確認用の画面へのパス
     */
    @GetMapping(path = "/index_post")
    public String index_post(Model model){
        // ユーザーデータリストを取得
        List<UserData> userDataList = getUserDataList();
        model.addAttribute("userDataList", userDataList);
        return "index_post";
    }

    /**
     * Post&Redirect&Get方式確認用の画面に遷移する
     * @model Modelオブジェクト
     * @return Post&Redirect&Get方式確認用の画面へのパス
     */
    @GetMapping(path = "/index_prg")
    public String index_prg(Model model){
        // ユーザーデータリストを取得
        List<UserData> userDataList = getUserDataList();
        model.addAttribute("userDataList", userDataList);
        return "index_prg";
    }

    /**
     * Post方式でデータを登録する
     * @param demoForm 登録用フォーム
     * @param model Modelオブジェクト
     * @return Post方式確認用の画面へのパス
     */
    @PostMapping(path = "/addDataPost")
    public String addDataPost(DemoForm demoForm, Model model){
        // 登録用フォームから送られてきたデータを登録する
        UserData userData = getUserData(demoForm);
        repository.saveAndFlush(userData);
        // Post方式確認用の初期表示画面に遷移する
        return index_post(model);
    }

    /**
     * Post&Redirect&Get方式でデータを登録する
     * @param demoForm 登録用フォーム
     * @param model Modelオブジェクト
     * @return Post方式確認用の画面へのパス
     */
    @PostMapping(path = "/addDataPostRedirectGet")
    public String addDataPostRedirectGet(DemoForm demoForm, Model model){
        // 登録用フォームから送られてきたデータを登録する
        UserData userData = getUserData(demoForm);
        repository.saveAndFlush(userData);
        // ユーザーデータリストを取得
        List<UserData> userDataList = getUserDataList();
        model.addAttribute("userDataList", userDataList);
        // Post&Redirect&Get方式確認用の初期表示画面に遷移する
        return "redirect:/index_prg";
    }

    /**
     * ユーザーリストを取得
     * @return ユーザーリスト
     */
    private List<UserData> getUserDataList(){
        // ユーザーデータをIDの昇順に取得し、取得できなければそのまま返す
        List<UserData> userDataList = repository.findAll(new Sort(ASC, "id"));
        // ユーザーデータが取得できなかった場合は、null値を返す
        if(userDataList == null || userDataList.size() == 0){
            return null;
        }
        for(UserData userData : userDataList){
            // 性別を表示用(男,女)に変換
            userData.setSex("1".equals(userData.getSex()) ? "男" : "女");
        }
        return userDataList;
    }

    /**
     * UserDataオブジェクトに引数のフォームの各値を設定する
     * @param demoForm DemoFormオブジェクト
     * @return ユーザーデータ
     */
    private UserData getUserData(DemoForm demoForm){
        UserData userData = new UserData();
        userData.setId(getMaxId() + 1);
        userData.setName(demoForm.getName());
        userData.setBirthY(Integer.valueOf(demoForm.getBirthYear()));
        userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth()));
        userData.setBirthD(Integer.valueOf(demoForm.getBirthDay()));
        userData.setSex(demoForm.getSex());
        userData.setMemo(demoForm.getMemo());
        return userData;
    }

    /**
     * 登録IDの最大値を取得する
     * @return 登録IDの最大値
     */
    private long getMaxId(){
        List<UserData> userDataList = repository.findAll(new Sort(DESC, "id"));
        if(userDataList == null || userDataList.size() == 0){
            return 0;
        }else{
            return userDataList.get(0).getId();
        }
    }
}



その他、フォームオブジェクトの内容は以下の通りで、今回はチェック処理を入れていない。

package com.example.demo;

import lombok.Data;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;

@Data
public class DemoForm implements Serializable {

    /** ID */
    private String id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private String birthYear;

    /** 生年月日_月 */
    private String birthMonth;

    /** 生年月日_日 */
    private String birthDay;

    /** 性別 */
    private String sex;

    /** メモ */
    private String memo;

    /** 確認チェック */
    private String checked;

    /** 性別(文字列) */
    private String sex_value;

    /** 生年月日_月のMapオブジェクト */
    public Map<String,String> getMonthItems(){
        Map<String, String> monthMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 12; i++){
            monthMap.put(String.valueOf(i), String.valueOf(i));
        }
        return monthMap;
    }

    /** 生年月日_日のMapオブジェクト */
    public Map<String,String> getDayItems(){
        Map<String, String> dayMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 31; i++){
            dayMap.put(String.valueOf(i), String.valueOf(i));
        }
        return dayMap;
    }

    /** 性別のMapオブジェクト */
    public Map<String,String> getSexItems(){
        Map<String, String> sexMap = new LinkedHashMap<String, String>();
        sexMap.put("1", "男");
        sexMap.put("2", "女");
        return sexMap;
    }

}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-prg-pattern/demo

サンプルプログラムの実行結果

Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスすると、以下の画面が表示される
実行結果_1

「Post方式確認用画面に遷移」ボタンを押下すると、下記画面が表示される
実行結果_2
この後の動作は、本記事の前方に記載した1)~4)を参照のこと。

また、「Post&Redirect&Get方式確認用画面に遷移」ボタンを押下すると、下記画面が表示される
実行結果_3
この後の動作は、本記事の前方に記載した5)~7)を参照のこと。

要点まとめ

  • PRGパターンとは、Postした後でリダイレクトして画面表示するパターンのことで、これを利用すると、ブラウザを再度読み込んだ場合に再度サブミット処理が実行されるのを防ぐことができる。
  • PRGパターンを利用するには、コントローラクラスでパスに「redirect:/(HTMLファイルへのパス)」を指定すればよい。