今回は、Spring Securityでエラーが発生した場合に、エラー画面に遷移する処理を追加してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
Spring Securityの設定を行うクラスの内容は以下の通りで、configure(HttpSecurity http)メソッド内に、エラー発生時にエラー画面に遷移する処理を追加している。
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.security.web.firewall.DefaultHttpFirewall; @Configuration @EnableWebSecurity public class DemoSecurityConfig extends WebSecurityConfigurerAdapter { /** * Spring-Security用のユーザーアカウント情報を * 取得・設定するサービスへのアクセス */ @Autowired private UserPassAccountService userDetailsService; @Override public void configure(WebSecurity web) { //org.springframework.security.web.firewall.RequestRejectedException: //The request was rejected because the URL contained a potentially malicious String ";" //というエラーログがコンソールに出力されるため、下記を追加 DefaultHttpFirewall firewall = new DefaultHttpFirewall(); web.httpFirewall(firewall); //セキュリティ設定を無視するリクエスト設定 //静的リソースに対するアクセスの場合は、Spring Securityのフィルタを通過しないように設定する web.ignoring().antMatchers("/css/**"); } /** * SpringSecurityによる認証を設定 * @param http HttpSecurityオブジェクト * @throws Exception 例外 */ @Override protected void configure(HttpSecurity http) throws Exception { final HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); //初期表示画面を表示する際にログイン画面を表示する http.formLogin() //ログイン画面は常にアクセス可能とする .loginPage("/login").permitAll() //ログインに成功したら検索画面に遷移する .defaultSuccessUrl("/") .and() //ログイン画面のcssファイルとしても共通のdemo.cssを利用するため、 //src/main/resources/static/cssフォルダ下は常にアクセス可能とする .authorizeRequests().antMatchers("/css/**").permitAll() .and() //かつ //それ以外の画面は全て認証を有効にする .authorizeRequests().anyRequest().authenticated() .and() //かつ //ログアウト時はログイン画面に遷移する .logout().logoutSuccessUrl("/login").permitAll() .and() //かつ //CSRFトークンのリポジトリを設定する .csrf().csrfTokenRepository(repository) .and() //かつ //エラー発生時はエラー画面に遷移する .exceptionHandling().accessDeniedPage("/toError") .and() //かつ //CSRFトークンのセッションキーをリクエスト毎に更新する処理を、 //CsrfFilterが呼ばれる前に実行するようにする .addFilterBefore(new UpdSessionCsrfFilter(repository), CsrfFilter.class) //CSRFトークンをリクエスト毎に更新する処理を、 //CsrfFilter(CSRFトークンチェックを行うFilter)が呼ばれた後に実行するようにする .addFilterAfter(new ChgCsrfTokenFilter(repository), CsrfFilter.class); } /** * 認証するユーザー情報をデータベースからロードする処理 * @param auth 認証マネージャー生成ツール * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //認証するユーザー情報をデータベースからロードする //その際、パスワードはBCryptで暗号化した値を利用する auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * パスワードをBCryptで暗号化するクラス * @return パスワードをBCryptで暗号化するクラスオブジェクト */ @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } } |
また、検索画面の内容は以下の通りで、「新しい画面を開く(エラー確認用)」ボタンを追加している。
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 82 83 84 85 86 87 88 89 90 91 92 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link th:href="@{/css/demo.css}" rel="stylesheet" type="text/css" /> <title>index page</title> </head> <body> <p>検索条件を指定し、「検索」ボタンを押下してください。</p><br/> <form method="post" th:action="@{/search}" th:object="${searchForm}"> <!-- 2行エラーがある場合は、エラーメッセージを改行して表示 --> <span th:if="*{#fields.hasErrors('fromBirthYear')}" th:errors="*{fromBirthYear}" class="errorMessage"></span> <span th:if="*{#fields.hasErrors('fromBirthYear') && #fields.hasErrors('toBirthYear')}"> <br/> </span> <span th:if="*{#fields.hasErrors('toBirthYear')}" th:errors="*{toBirthYear}" class="errorMessage"></span> <table border="1" cellpadding="5"> <tr> <th>名前</th> <td><input type="text" th:value="*{searchName}" th:field="*{searchName}" /></td> </tr> <tr> <th>生年月日</th> <td><input type="text" th:value="*{fromBirthYear}" size="4" maxlength="4" th:field="*{fromBirthYear}" th:errorclass="fieldError"/>年 <select th:field="*{fromBirthMonth}" th:errorclass="fieldError" th:classappend="${#fields.hasErrors('fromBirthYear')} ? 'fieldError'"> <option value=""></option> <option th:each="item : *{getMonthItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>月 <select th:field="*{fromBirthDay}" th:errorclass="fieldError" th:classappend="${#fields.hasErrors('fromBirthYear')} ? 'fieldError'"> <option value=""></option> <option th:each="item : *{getDayItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>日~ <input type="text" th:value="*{toBirthYear}" size="4" maxlength="4" th:field="*{toBirthYear}" th:errorclass="fieldError"/>年 <select th:field="*{toBirthMonth}" th:errorclass="fieldError" th:classappend="${#fields.hasErrors('toBirthYear')} ? 'fieldError'"> <option value=""></option> <option th:each="item : *{getMonthItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>月 <select th:field="*{toBirthDay}" th:errorclass="fieldError" th:classappend="${#fields.hasErrors('toBirthYear')} ? 'fieldError'"> <option value=""></option> <option th:each="item : *{getDayItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>日 </td> </tr> <tr> <th>性別</th> <td> <select th:field="*{searchSex}"> <option value=""></option> <option th:each="item : *{getSexItems()}" th:value="${item.key}" th:text="${item.value}"/> </select> </td> </tr> </table> <br/><br/> <input type="submit" value="検索" /> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> </form> <br/><br/> <!-- 新しい画面を開く処理 --> <!-- 画面を開くタイミングでは、CSRFトークンチェックを行わないよう、GETメソッドを指定する --> <form target="newwindow" th:action="@{/openNewWindow1}" method="GET"> <input type="hidden" name="windowName" value="new_window1" /> <button type="submit">新しい画面を開く</button> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/><br/> </form> <br/><br/> <!-- 新しい画面を開く処理(エラー確認用) --> <form target="newwindow" th:action="@{/openNewWindow1Error}" method="GET"> <input type="hidden" name="windowName" value="new_window1_error" /> <button type="submit">新しい画面を開く(エラー確認用)</button> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/><br/> </form> <br/><br/> <form method="post" th:action="@{/logout}"> <button type="submit">ログアウト</button> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> </form> </body> </html> |
さらに、「新しい画面を開く処理(エラー確認用)」ボタン押下時に開く画面の内容は以下の通りで、windowNameをパラメータに設定しないことで、「次へ」ボタン押下時にエラーが発生するようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>新規オープン画面1</title> </head> <body> 新しい画面を開きました。 こちらは「次へ」ボタンを押下するとエラーが発生します。<br/><br/> <form th:action="@{/toNewWindow2Error}" method="POST"> <!-- エラーを発生させるため、下記をコメントアウト --> <!-- <input type="hidden" name="windowName" value="new_window1_error" /> --> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/><br/> <br/><br/> <button type="submit">次へ</button> </form> </body> </html> |
また、「new_window1_error.html」で「次へ」ボタン押下時にエラーが発生しない場合の、遷移先画面の内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>新規オープン画面2</title> </head> <body> 新規オープン画面から画面遷移しました。 <br/><br/> <input type="text" style="background-color:#FFCCCC" size="80" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/><br/><br/> <input type="button" value="閉じる" onclick="window.close();" /> </body> </html> |
さらに、エラー画面の内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>エラー画面</title> </head> <body> エラーが発生しました。処理を終了します。<br/><br/> <input type="button" value="閉じる" onclick="window.close();" /> </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 | /** * 新しい画面を開く(エラー確認用) * @return 新規オープン画面1(エラー確認用)へのパス */ @GetMapping("/openNewWindow1Error") public String openNewWindow1Error(){ return "new_window1_error"; } /** * 新規オープン画面2(エラー確認用)に遷移する * @return 新規オープン画面2(エラー確認用)へのパス */ @PostMapping("/toNewWindow2Error") public String toNewWindow2Error(){ return "new_window2_error"; } /** * エラー画面に遷移する * @return エラー画面へのパス */ @RequestMapping("/toError") public String toError(){ return "error"; } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-security-error-handling/demo
サンプルプログラムの実行結果
Spring Securityの設定を行うクラスでエラーハンドリングを行う前の動作は以下の通りで、エラー時はSpring Bootデフォルトのホワイトラベルエラーページが表示される。
1) Spring Bootアプリケーションを起動し、「http://(サーバー名):(ポート番号)/」とアクセスすると、以下のログイン画面が表示されるので、ユーザー名・パスワードを入力し「ログイン」ボタンを押下
2) 検索画面に遷移するので、「新しい画面を開く」ボタンを押下
3) 以下のように、以前の画面とは別に「新規オープン画面1」が開くことが確認できるので、「次へ」ボタンを押下
4) 以下のように、新規オープン画面2に遷移するので、「閉じる」ボタンを押下
5) 以下のように、確認ダイアログが表示されるので、「はい」ボタンを押下
6) 検索画面で、「新しい画面を開く(エラー確認用)」ボタンを押下
7) 「新規オープン画面1」が開くことが確認できるので、「次へ」ボタンを押下
8) エラーハンドリングを行っていないので、以下のように、Spring Bootデフォルトのホワイトラベルエラーページが表示される
また、Spring Securityの設定を行うクラスでエラーハンドリングを行った後の動作は以下の通りで、エラー時はエラー画面に遷移する。
1) Spring Bootアプリケーションを起動し、「http://(サーバー名):(ポート番号)/」とアクセスすると、以下のログイン画面が表示されるので、ユーザー名・パスワードを入力し「ログイン」ボタンを押下
2) 検索画面に遷移するので、「新しい画面を開く(エラー確認用)」ボタンを押下
3) 「新規オープン画面1」が開くことが確認できるので、「次へ」ボタンを押下
4) エラーハンドリングによって、以下のエラー画面に遷移することが確認できる
要点まとめ
- Spring Securityでエラーが発生した場合に、エラー画面に遷移する処理を追加するには、Spring Securityの設定を行うクラスの内のconfigure(HttpSecurity http)メソッド内で、「exceptionHandling().accessDeniedPage(“/エラー画面へのパス”)」を追加すればよい。