Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その23 )( Spring Security 関連で修正した方がよい箇所を見直す )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる問題を解消します。
- 他に Spring Security 関連で修正した方がよい箇所を見直すつもりでいましたが、上記の1点以外は修正しません。いろいろ Web の記事等を見ましたが、よく分かりませんでした。動作に支障はなさそうなので、このまま行くことにします。
参照したサイト・書籍
Spring Security 4.1 主な変更点
http://qiita.com/kazuki43zoo/items/e925f134e65d7595aa3cSpring Security 4.2 主な変更点
http://qiita.com/kazuki43zoo/items/ef6cc6d05d68a8cb7a0e
目次
手順
ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる原因を調査する
bootRun で Tomcat を起動してからログイン画面を表示し、何も入力せずに「ログイン」ボタンをクリックするとエラー画面が表示されることに気づきました。Spring Boot を 1.2 → 1.3 にバージョンアップした時からこの現象が出ていたようです。
以下のログが出力されていました。
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal" (login:63)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'authentication' cannot be found on object of type 'org.springframework.security.authentication.BadCredentialsException' - maybe not public?
ログインエラー時に入力された ID を入力されたままにしようとして session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal
にアクセスしていたのですが、.authentication
にアクセスできなくなっていることが原因のようです。
Spring Boot の 1.2 を使用していた時に戻して、その時にはなぜエラーが出ないのかを確認してみたところ、
- ログインエラー時に
session['SPRING_SECURITY_LAST_EXCEPTION']
にセットされていたのはorg.springframework.security.authentication.BadCredentialsException
のインスタンスである。 BadCredentialsException
はorg.springframework.security.core.AuthenticationException
の継承クラスである。org.springframework.security.core.AuthenticationException
にはgetAuthentication
メソッドが実装されていた。ただし@Deprecated
アノテーションが付いていました。。。 Thymeleaf からアクセスしているだけだと気づきませんでした。
という訳で、この時にはまだ session['SPRING_SECURITY_LAST_EXCEPTION'].authentication
にアクセスできていたからでした。Spring Boot の 1.4 に戻してみると、org.springframework.security.core.AuthenticationException
から getAuthentication
メソッドが見事になくなっています。
ログインエラー時に ID を入力されたままにする
ログイン画面で何も入力せずに「ログイン」ボタンをクリックした時にエラーになる件は、単純に session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal
を削除してアクセスしなければ回避できますが、それでは入力された ID がログインエラー時に消えてしまいます。
ログインエラー時でも、入力した ID が入力されたままになるようにしてみます。
Spring Security 4.1 主な変更点 の記事を見ると、Spring Security 4.1 で AuthenticationFailureHandler にて遷移先にフォワードする実装クラスが追加されたと記載されていました。
IntelliJ IDEA で AuthenticationFailureHandler インターフェースの実装クラスを調べて見ると以下の図のようになっていました。ForwardAuthenticationFailureHandler クラスが遷移先にフォワードする実装クラスのようです。これを使用してみます。
src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java を リンク先の内容 に変更します。
src/main/resources/templates/login.html を リンク先の内容 に変更します。
ForwardAuthenticationFailureHandler クラスに変更するとログインエラー発生時にリダイレクトされなくなり、HTTPステータスコードが 302 ではなく 200 が返るようになります。src/test/java/ksbysample/webapp/lending/web/LoginControllerTest.java の以下のテストメソッドを変更します。
- 変更するテストメソッド
- パスワードの有効期限が切れているユーザならばログインはエラーになる()
- 存在しないユーザ名とパスワードを入力すればログインはエラーになる()
- 存在するユーザ名でもパスワードが正しくなければログインはエラーになる()
- アカウントの有効期限が切れているユーザならばログインはエラーになる()
- ログインを5回失敗すればアカウントはロックされる()
- enabledが0のユーザならばログインはエラーになる()
- 変更内容
.andExpect(status().isFound())
→.andExpect(status().isOk())
へ変更する。.andExpect(redirectedUrl("/"))
を削除する。.andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(DisabledException.class)));
→.andExpect(request().attribute("SPRING_SECURITY_LAST_EXCEPTION", isA(DisabledException.class)));
へ変更する。
- 変更するテストメソッド
bootRun で Tomcat を起動して動作を確認してみます。
ログイン画面を表示して何も入力せずに「ログイン」ボタンをクリックをすると、エラー画面は表示されずログイン画面上にエラーメッセージが表示されました。
ログイン画面を表示し直して ID に “test” とだけ入力して「ログイン」ボタンをクリックをすると、ログイン画面上にエラーメッセージが表示され、入力した “test” は入力された状態で表示されました。ここまでは期待通りの動作です。
また login.html で入力された ID を表示するために
${#httpServletRequest.getParameter('id')}
で出力するようにしましたが、これで XSS対策が問題ないのか試してみます。http://localhost:8080/?id=<script>alert('hello');</script>
でアクセスすると HTTPステータスコードの 400 が返ってきました。ログを見ると
java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:471)
というメッセージが出力されています。Spring Security ではなく org.apache.coyote.http11.Http11InputBuffer クラスが出力しています。org.apache.coyote.http11.Http11InputBuffer クラスが何のコンポーネントに含まれているのか調べてみると、org.apache.tomcat.embed:tomcat-embed-core:8.5.11 に入っているクラスでした。
RFC 7230, 3986 は以下のリンクに日本語訳があります。
HTTPステータスコードの 400 が返ってきたのは、
<
等が URI には直接使用できる文字ではないので、リクエストで送信されてくると Tomcat がエラーにするためのようです。起動していた Tomcat を停止します。
ソースコード
WebSecurityConfig.java
@Override protected void configure(HttpSecurity http) throws Exception { .......... http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL) .usernameParameter("id") .passwordParameter("password") .successHandler(new RoleAwareAuthenticationSuccessHandler()) .failureHandler(new ForwardAuthenticationFailureHandler("/")) .permitAll() ..........
.failureUrl("/")
を削除します。.failureHandler(new ForwardAuthenticationFailureHandler("/"))
を追加します。
login.html
<form role="form" action="#" th:action="@{/login}" method="post" id="login-form" autocomplete="off"> <div class="form-group" th:if="${#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION')} != null"> <p class="form-control-static text-danger" th:text="${#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION').message}"></p> </div> <div class="form-group"> <label for="id" class="sr-only">ID</label> <input type="text" name="id" id="id" class="form-control" placeholder="ID を入力して下さい" th:value="${#httpServletRequest.getParameter('id')} != null ? ${#httpServletRequest.getParameter('id')} : ''"/> </div> <div class="form-group"> <label for="password" class="sr-only">Password</label> <input type="password" name="password" id="password" class="form-control" placeholder="Password を入力して下さい"/> </div> <div class="form-group text-center"> <div class="checkbox"> <label><input type="checkbox" name="remember-me" id="remember-me" value="true"/>次回から自動的にログインする</label> </div> </div> <button id="btn-login" class="btn btn-primary btn-block">ログイン</button> </form>
- 遷移先にフォワードする実装クラス(ForwardAuthenticationFailureHandler)に変更した場合、ログインエラー時の例外はセッションではなくリクエストスコープにセットされますので、
session['SPRING_SECURITY_LAST_EXCEPTION']
→#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION')
に変更します。 - ID を表示する th:value の記述を
${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''
→${#httpServletRequest.getParameter('id')} != null ? ${#httpServletRequest.getParameter('id')} : ''
へ変更します。
履歴
2017/05/03
初版発行。