読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その23 )( Spring Security 関連で修正した方がよい箇所を見直す )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その22 )( application.properties に記述する spring.datasource.tomcat.~ の設定を見直す ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる問題を解消します。
    • 他に Spring Security 関連で修正した方がよい箇所を見直すつもりでいましたが、上記の1点以外は修正しません。いろいろ Web の記事等を見ましたが、よく分かりませんでした。動作に支障はなさそうなので、このまま行くことにします。

参照したサイト・書籍

  1. Spring Security 4.1 主な変更点
    http://qiita.com/kazuki43zoo/items/e925f134e65d7595aa3c

  2. Spring Security 4.2 主な変更点
    http://qiita.com/kazuki43zoo/items/ef6cc6d05d68a8cb7a0e

目次

  1. ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる原因を調査する
  2. ログインエラー時に ID を入力されたままにする

手順

ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる原因を調査する

bootRun で Tomcat を起動してからログイン画面を表示し、何も入力せずに「ログイン」ボタンをクリックするとエラー画面が表示されることに気づきました。Spring Boot を 1.2 → 1.3 にバージョンアップした時からこの現象が出ていたようです。

f:id:ksby:20170425002830p:plain f:id:ksby:20170425002927p:plain

以下のログが出力されていました。

  • 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インスタンスである。
  • BadCredentialsExceptionorg.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 クラスが遷移先にフォワードする実装クラスのようです。これを使用してみます。

f:id:ksby:20170503155411p:plain

  1. src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.javaリンク先の内容 に変更します。

  2. src/main/resources/templates/login.html を リンク先の内容 に変更します。

  3. 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))); へ変更する。
  4. bootRun で Tomcat を起動して動作を確認してみます。

    ログイン画面を表示して何も入力せずに「ログイン」ボタンをクリックをすると、エラー画面は表示されずログイン画面上にエラーメッセージが表示されました。

    f:id:ksby:20170503164339p:plain f:id:ksby:20170503164424p:plain

    ログイン画面を表示し直して ID に “test” とだけ入力して「ログイン」ボタンをクリックをすると、ログイン画面上にエラーメッセージが表示され、入力した “test” は入力された状態で表示されました。ここまでは期待通りの動作です。

    f:id:ksby:20170503164707p:plain f:id:ksby:20170503164820p:plain

    また login.html で入力された ID を表示するために ${#httpServletRequest.getParameter('id')} で出力するようにしましたが、これで XSS対策が問題ないのか試してみます。

    http://localhost:8080/?id=<script>alert('hello');</script> でアクセスすると HTTPステータスコードの 400 が返ってきました。

    f:id:ksby:20170503165646p:plain

    ログを見ると 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 に入っているクラスでした。

    f:id:ksby:20170503170800p:plain

    RFC 7230, 3986 は以下のリンクに日本語訳があります。

    HTTPステータスコードの 400 が返ってきたのは、< 等が URI には直接使用できる文字ではないので、リクエストで送信されてくると Tomcat がエラーにするためのようです。

  5. 起動していた 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
初版発行。