WEBアプリケーション上の画面にアクセスする際、そのアクセスが不正でないかチェックする方法として、リクエストトークンという何らかの文字列を利用して、セッション上のリクエストトークンと画面上のリクエストトークンが一致しているかどうかチェックする方法がある。
今回は、リクエストトークンチェックを含むサンプルプログラムを作成してみたので、共有する。
前提条件
下記記事の実装が完了していること
完成した画面イメージ
今回作成したサンプルプログラムの画面イメージは、以下の通り。
1) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスすると、以下の画面が表示されるので、「次へ」ボタンを押下

2) next.html画面に遷移し、リクエストトークンの値が変わることが確認できるので、「戻る」ボタンを押下

3) index.html画面に戻り、リクエストトークンの値が再度変わることが確認できるので、更新ボタンを押下

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

サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
リクエストトークンのチェック処理を生成を行う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);
}
}
また、リクエストが実行される度に先ほどの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
なお、今回のサンプルプログラムは、複数のウィンドウを開く場合に対応できていない。複数のウィンドウを開く場合に対応するプログラムについては、以下の記事を参照のこと。
要点まとめ
- WEBアプリケーションへのアクセスが不正でないかチェックする方法として、リクエストトークンをチェックする方法がある。
- リクエストトークンは画面上とセッション上にもたせた上で、HandlerInterceptorインタフェースを実装したクラスで、リクエストトークンのチェックや生成を行えばよい。






