WEBアプリケーション上の画面にアクセスする際、そのアクセスが不正でないかチェックする方法として、リクエストトークンという何らかの文字列を利用して、セッション上のリクエストトークンと画面上のリクエストトークンが一致しているかどうかチェックする方法がある。
今回は、リクエストトークンチェックを含むサンプルプログラムを作成してみたので、共有する。
前提条件
下記記事の実装が完了していること
完成した画面イメージ
今回作成したサンプルプログラムの画面イメージは、以下の通り。
1) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスすると、以下の画面が表示されるので、「次へ」ボタンを押下
2) next.html画面に遷移し、リクエストトークンの値が変わることが確認できるので、「戻る」ボタンを押下
3) index.html画面に戻り、リクエストトークンの値が再度変わることが確認できるので、更新ボタンを押下
5) 2)の画面の「戻る」ボタン押下時の処理が再度実行され、下記トークンチェックエラーの画面に遷移することが確認できる
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
リクエストトークンのチェック処理を生成を行うInterceptorクラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 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クラスが呼ばれるような設定の内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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); } } |
さらに、エラー画面に遷移するためのコントローラクラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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の内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!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の内容を読み込んでいる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!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> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!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の内容は、以下の通り。
1 2 3 4 5 6 7 8 9 | <!DOCTYPE html> <html lang="ja"> <meta charset="UTF-8"> <title>error page</title> </head> <body> トークンチェックエラーが発生しました。 </body> </html> |
その他、コントローラクラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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インタフェースを実装したクラスで、リクエストトークンのチェックや生成を行えばよい。