かんがるーさんの日記

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

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その9 )( ログイン画面作成2 )

概要

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その8 )( ログイン画面作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • パスワードを暗号化します。
    • 検索/一覧画面のページネーション用に作成した form に自動で CSRF対策用の hidden が出力されない原因を調査します。
    • ログインエラー時に ID がクリアされるので、ID を残す方法があるか確認します。
  • 下記の件はいろいろ調べましたが、そもそも実現する必要性がなさそうなので調べるのを止めました。調査中に分かったことで残しておきたい点だけ書いています。
    • 最初に呼ばれた認証を必要とする URL が存在しない URL の場合にはログイン画面を表示せずに最初から 404 を返すか、ログインした後デフォルトページ ( http://localhost:8080/countryList ) に遷移させる方法を確認します。
  • 下記の件は調べた結果ちょっと時間かかりそうな感じだったので、後日改めて実装することにします。こちらもメモだけ残しておきます。
    • エラーメッセージで用意されている locked, expired, credentialsExpired を出す方法を確認してみます。

ソフトウェア一覧

参考にしたサイト

  1. Kielczewski.eu - Spring Boot Security Application
    http://kielczewski.eu/2014/12/spring-boot-security-application/

  2. Spring Security - 2.3. Password Encoding
    http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#core-services-password-encoding

  3. SPRING_SECURITY_LAST_USERNAME NOT AVAILABLE WHEN ALLOWSESSIONCREATION = "FALSE"
    http://forum.spring.io/forum/spring-projects/security/98547-spring-security-last-username-not-available-when-allowsessioncreation-false

  4. Redirect to different pages after Login with Spring Security
    http://www.baeldung.com/spring_redirect_after_login

    • ログインしたユーザの権限に応じて、ログイン後の画面を変える記事です。
    • 今回の実装には反映していませんが、参考になりそうなのでメモしておきます。
  5. Testing login in Spring Security
    http://stackoverflow.com/questions/23980762/testing-login-in-spring-security

    • こちらも successHandler を変更する方法について記載されています。
    • 今回の実装には反映していませんが、参考になりそうなのでメモしておきます。
  6. Spring Framework Advent Calendar 2011 part.7 - Spring Security で認証成功時に条件によって遷移先を変えるCommentsAdd Star
    http://d.hatena.ne.jp/ocs/20111207/1323269801

  7. Spring Security でアカウントロック機構を実現する
    http://d.hatena.ne.jp/ocs/20111101/1320115388
  8. Spring Security : limit login attempts example
    http://www.mkyong.com/spring-security/spring-security-limit-login-attempts-example/

    • 上の3つの記事は Spring Security の locked, expired, credentialsExpired を出す方法をを調べていた時に見つけたものです。
    • 今回の実装には反映していませんが、参考になりそうなのでメモしておきます。

手順

パスワードを暗号化する

  1. IntelliJ IDEA 上で 1.0.x-encodepassword ブランチを作成します。

  2. 動作確認のために暗号化した文字列を生成する必要がありますので、生成用の URL http://localhost:8080/encode?password=... を作成します。暗号化アルゴリズムは Spring Security のマニュアルに記載されている bcrypt を使用します。

  3. src/main/java/ksbysample/webapp/basic/web の下の LoginController.java をエディタで開き、リンク先の内容 に変更します。

  4. http://localhost:8080/encode?password=... を認証対象外にします。src/main/java/ksbysample/webapp/basic/config の下の WebSecurityConfig.java をエディタで開き、リンク先のその1の内容に変更します。

  5. 動作確認します。Gradle tasks View から bootRun を実行し、ブラウザで http://localhost:8080/encode?password=ptest にアクセスすると暗号化された文字列が表示されます。Ctrl+F2 を押して Tomcat を停止します。

    f:id:ksby:20150129223552p:plain

  6. user テーブルの id = 'test' のデータの password の値をリンク先のSQL を実行して更新します。

  7. ログイン時の認証処理で暗号化パスワードを使用するようにします。src/main/java/ksbysample/webapp/basic/config の下の WebSecurityConfig.java をエディタで開き、リンク先のその2の内容に変更します。

  8. ログインできるか確認します。Gradle tasks View から bootRun を実行し、ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、test / ptest を入力して「ログイン」ボタンをクリックします。認証が通り http://localhost:8080/countryList に遷移します。Ctrl+F2 を押して Tomcat を停止します。

  9. ちなみに http://localhost:8080/encode?password=ptest はアクセスする度に異なる文字列が表示されますが、どの文字列を user テーブルに保存しても test / ptest でログインできました。保存するパスワードの暗号化文字列が違っても test / ptest できちんと認証される仕組みがよく分からないんですよね。。。 そのうち調べたいと思います。

  10. commit、GitHub へ Push、1.0.x-encodepassword -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-encodepassword ブランチを削除、をします。

検索/一覧画面のページネーション用に作成した form に自動で CSRF対策用の hidden が出力されない原因を調査する

  1. IntelliJ IDEA 上で 1.0.x-checkcsrftag ブランチを作成します。

  2. src/main/resources/templates の下の countryList.html 内には form が2つありますが、上の form には自動で _csrf の input タグが埋め込まれますが、下の form には埋め込まれません。違いを確認すると th:action の記述があるかないかだけのように見えますので、下の form タグにも th:action を記述してみます。countryList.html をエディタで開き、リンク先の内容 に変更します。

  3. Gradle tasks View から bootRun を実行して動作確認すると、ページネーションは問題なく動作し、_csrf の input タグも埋め込まれていました。Ctrl+F2 を押して Tomcat を停止します。

  4. commit、GitHub へ Push、1.0.x-checkcsrftag -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-checkcsrftag ブランチを削除、をします。

ログインエラー時に ID とパスワードがクリアされるが、ID は残す

  1. IntelliJ IDEA 上で 1.0.x-keepid ブランチを作成します。

  2. src/main/resources/templates の下の login.html をエディタで開き、リンク先のその1、その2の内容 に変更します。

  3. 動作確認します。Gradle tasks View から bootRun を実行し、ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、test / test を入力して「ログイン」ボタンをクリックします。認証エラーになりますが ID の入力フィールドには test の文字列が表示されています。Ctrl+F2 を押して Tomcat を停止します。

  4. commit、GitHub へ Push、1.0.x-keepid -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-keepid ブランチを削除、をします。

最初に呼ばれた認証を必要とする URL が存在しない URL の場合にはログイン画面を表示せずに最初から 404 を返すか、ログインした後デフォルトページ ( http://localhost:8080/countryList ) に遷移させる

※メモだけです。必ずログイン後はデフォルトページ ( http://localhost:8080/countryList ) に遷移させる方法は分かりました。src/main/java/ksbysample/webapp/basic/config の下の WebSecurityConfig.java をエディタで開き、リンク先のその3の内容 に変更すると常にデフォルトページ ( http://localhost:8080/countryList ) に遷移します。今回反映はしません。

エラーメッセージで用意されている locked, expired, credentialsExpired を出す方法を確認する

※メモだけです。JDBC認証用の UserDetailsService クラスのソース を見たら、loadUserByUsername メソッドは createUserDetails メソッドを呼び出して UserDetails クラスのインスタンスを生成している → createUserDetails メソッドは User クラスのインスタンスを生成する時に expired, locked, credentialsExpired 用の値をセットするところに全部 true をセットしている、のでデフォルトではこの3つは必ず true で何も実装されていない、ということは分かりました。UserDetailsService を継承したクラスを作る必要があります。

次回は。。。

  • 今回の作業中に検索/一覧画面の Bean Validation がうまく動作していないことに気づきました。原因を調査します。
  • テストクラスを書きます。

ソースコード

LoginController.java

package ksbysample.webapp.basic.web;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class LoginController {

    @RequestMapping("/")
    public String index() {
        return "login";
    }

    @RequestMapping("/encode")
    @ResponseBody
    public String encode(@RequestParam String password) {
        return new BCryptPasswordEncoder().encode(password);
    }

}

WebSecurityConfig.java

■その1

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//                認証の対象外にしたいURLがある場合には、以下のような記述を追加します
//                複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します
//                .antMatchers("/country/**").permitAll()
                .antMatchers("/encode").permitAll()
                .anyRequest().authenticated();
  • .antMatchers("/encode").permitAll() を追加します。

■その2

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
                .usersByUsernameQuery("select id, password, enabled from user where id = ?")
                .authoritiesByUsernameQuery("select id, role from user_role where id = ?")
                .passwordEncoder(new BCryptPasswordEncoder());
    }
  • .passwordEncoder(new BCryptPasswordEncoder()) を追加します。

■その3

        http.formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/countryList", true)
  • .defaultSuccessUrl("/countryList").defaultSuccessUrl("/countryList", true) へ変更します。

update.sql

update user
set password = '$2a$10$Ixr3i98D9Lt2VFs5t6wMIOljZ0JDR5Q7s7lPOSlm0.u460oGtntIO'
where id = 'test';
commit;

countryList.html

        <form id="pagenationForm" method="post" action="#" th:action="@{#}" th:object="${countryListForm}">
            <input type="hidden" name="code" id="code" th:value="*{code}"/>
            <input type="hidden" name="name" id="name" th:value="*{name}"/>
            <input type="hidden" name="continent" id="continent" th:value="*{continent}"/>
            <input type="hidden" name="localName" id="localName" th:value="*{localName}"/>
        </form>
  • th:action="@{#}" を追加します。
  • <input type="hidden" name="_csrf" th:value="${_csrf.token}"/> を削除します。

login.html

■その1

                            <input type="text" name="id" id="id" class="form-control" placeholder="ID を入力して下さい"
                                   th:value="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''"/>
  • id の input タグに th:value="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''" を追加します。

■その2

    <script type="text/javascript">
    <!--
    $(document).ready(function() {
        $('#id').focus().select();
    });
    -->
    </script>
  • id の入力フィールドにカーソルが移動した時に全選択の状態になっていなかったので、$('#id').focus();$('#id').focus().select(); へ変更します。

履歴

2015/02/01
初版発行。