Spring Boot セキュリティ関連

Spring BootのWEB画面上でリクエストトークンチェックを実行してみた

WEBアプリケーション上の画面にアクセスする際、そのアクセスが不正でないかチェックする方法として、リクエストトークンという何らかの文字列を利用して、セッション上のリクエストトークンと画面上のリクエストトークンが一致しているかどうかチェックする方法がある。

今回は、リクエストトークンチェックを含むサンプルプログラムを作成してみたので、共有する。

前提条件

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

IntelliJ IDEA上でGradleを使ってWeb画面のSpring Bootプロジェクトを作成してみたSpring Bootのプロジェクトを新規作成を「IntelliJ IDEA」のメニューから実施しようとしたところ、無料の「Commun...

完成した画面イメージ

今回作成したサンプルプログラムの画面イメージは、以下の通り。

1) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスすると、以下の画面が表示されるので、「次へ」ボタンを押下
サンプルプログラムの実行_1

2) next.html画面に遷移し、リクエストトークンの値が変わることが確認できるので、「戻る」ボタンを押下
サンプルプログラムの実行_2

3) index.html画面に戻り、リクエストトークンの値が再度変わることが確認できるので、更新ボタンを押下
サンプルプログラムの実行_3

4) 下記画面が表示されるので、「再試行」を押下
サンプルプログラムの実行_4

5) 2)の画面の「戻る」ボタン押下時の処理が再度実行され、下記トークンチェックエラーの画面に遷移することが確認できる
サンプルプログラムの実行_5

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

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

リクエストトークンのチェック処理を生成を行うInterceptorクラスの内容は、以下の通り。

package com.example.demo.base;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Random;

public class RequestTokenInterceptor implements HandlerInterceptor {

    /**
     * エラー画面へのパス
     */
    private static final String ERROR_PATH = "/error";

    /**
     * 乱数生成クラスのインスタンス
     */
    @Autowired
    private Random random;

    //コントローラクラスの画面遷移処理が呼ばれる前に実行されるメソッド
    @Override
    public boolean preHandle(HttpServletRequest request
            , HttpServletResponse response, Object handler) throws Exception {
        // エラー画面に遷移する場合以外は、リクエストトークンのチェックを行う
        if (!ERROR_PATH.equals(request.getRequestURI())) {
            // セッションに格納したリクエストトークンを取得
            String sesReqToken = null;
            HttpSession session = request.getSession(false);
            if (session != null) {
                sesReqToken = (String) session.getAttribute("sesReqToken");
            }
            // リクエストパラメータからのリクエストトークンを取得
            String reqToken = request.getParameter("reqToken");
            // リクエストトークンが一致しない場合、エラー画面に遷移
            if (!isEqualString(sesReqToken, reqToken)) {
                response.sendRedirect("/error");
                return false;
            }
            return true;
        }
        return true;
    }

    //コントローラクラスの画面遷移処理が呼ばれた後に実行されるメソッド
    @Override
    public void postHandle(HttpServletRequest request
            , HttpServletResponse response, Object handler
            , @Nullable ModelAndView modelAndView) {
        // リクエストトークンを生成し設定
        String reqToken = String.valueOf(random.nextInt(100000000));
        modelAndView.addObject("reqToken", reqToken);
        //生成したリクエストトークンをセッションに格納
        HttpSession session = request.getSession(false);
        if (session == null) {
            session = request.getSession(true);
        }
        session.setAttribute("sesReqToken", reqToken);
    }

    /**
     * 引数の文字列が一致するかどうか返却する
     *
     * @param str1 比較対象文字列1
     * @param str2 比較対象文字列2
     * @return 比較結果
     */
    private boolean isEqualString(String str1, String str2) {
        if (str1 == null && str2 == null) {
            return true;
        } else if (str1 == null && str2 != null) {
            return false;
        }
        return str1.equals(str2);
    }
}



フリーランスエンジニアのエージェントは就業中でも無料で登録できるITエンジニアには、フリーランスという働き方がある。 フリーランスとは、会社や団体などに所属せず、仕事に応じて自由に契約する人のこ...

また、リクエストが実行される度に先ほどのInterceptorクラスが呼ばれるような設定の内容は、以下の通り。

package com.example.demo.base;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Random;

@Configuration
public class DemoConfigBean {

    /**
     * リクエストトークンのInterceptorをBean定義に追加
     * @return リクエストトークンのInterceptor
     */
    @Bean
    public HandlerInterceptor requestTokenInterceptor() {
        return new RequestTokenInterceptor();
    }

    /**
     * 乱数生成クラスのインスタンスをBean定義に追加
     * @return 乱数生成クラスのインスタンス
     */
    @Bean
    public Random makeRandom() {
        return new Random();
    }
}
package com.example.demo.base;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class DemoWebMvcConfigurer implements WebMvcConfigurer {

    /**
     * リクエストトークンのInterceptor
     */
    @Autowired
    private HandlerInterceptor requestTokenInterceptor;

    /**
     * 作成したInterceptorをSpringに認識させる
     * @param registry Interceptorリポジトリ
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestTokenInterceptor);
    }
}



さらに、エラー画面に遷移するためのコントローラクラスの内容は、以下の通り。

package com.example.demo.base;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DemoErrorController implements ErrorController {

    /**
     * エラーが発生した場合の画面遷移
     * @return エラー画面へのパス
     */
    @RequestMapping("/error")
    public String to_error() {
        return "error";
    }

    /**
     * エラーパスを取得
     * (このメソッドを追加しないとコンパイルエラーになるため追加)
     * @return エラーパス
     */
    @Override
    public String getErrorPath() {
        return "";
    }
}



また、リクエストトークンを設定するfragment.htmlの内容は、以下の通り。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>fragment</title>
</head>
<body>
<div th:fragment="body-tag">
    <p>設定したリクエストトークン</p>
    <input type="text" name="reqToken" th:value="${reqToken}" />
</div>
</body>
</html>

さらに、index.html、next.htmlの内容は、以下の通りで、fragment.htmlのbody-tagの内容を読み込んでいる。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <h3>これはindex.html画面です</h3>
    <form method="post" th:action="@{/next}">
        <div th:insert="fragment :: body-tag"></div>
        <br/><br/>
        <input type="submit" value="次へ" />
    </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>next page</title>
</head>
<body>
    <h3>これはnext.html画面です</h3>
    <form method="post" th:action="@{/back}">
        <div th:insert="fragment :: body-tag"></div>
        <br/><br/>
        <input type="submit" value="戻る" />
    </form>
</body>
</html>

さらに、エラー画面のHTMLの内容は、以下の通り。

<!DOCTYPE html>
<html lang="ja">
    <meta charset="UTF-8">
    <title>error page</title>
</head>
<body>
    トークンチェックエラーが発生しました。
</body>
</html>



その他、コントローラクラスの内容は、以下の通り。

package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class DemoController {

    /**
     * 初期表示画面に遷移する
     * @return 初期表示画面へのパス
     */
    @GetMapping("/")
    public String index() {
        return "index";
    }

    /**
     * 次画面に遷移する
     * @return 次画面へのパス
     */
    @PostMapping("/next")
    public String next() {
        return "next";
    }

    /**
     * 初期表示画面に戻る
     * @return 初期表示画面へのパス
     */
    @PostMapping("/back")
    public String back() {
        return "index";
    }
}

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

なお、今回のサンプルプログラムは、複数のウィンドウを開く場合に対応できていない。複数のウィンドウを開く場合に対応するプログラムについては、以下の記事を参照のこと。

Spring Boot上でリクエスト毎にCSRFトークンを変える処理を複数画面を開く場合に対応してみた以前、Spring Securityを利用して、さらにリクエスト毎にCSRFトークンの値を変更するようにしたことがあったが、このプログラ...

要点まとめ

  • WEBアプリケーションへのアクセスが不正でないかチェックする方法として、リクエストトークンをチェックする方法がある。
  • リクエストトークンは画面上とセッション上にもたせた上で、HandlerInterceptorインタフェースを実装したクラスで、リクエストトークンのチェックや生成を行えばよい。