かんがるーさんの日記

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

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その27 )( ProviderManager#getProviders が DaoAuthenticationProvider を3つ返す原因を調査する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その26 )( Gradle を 4.10.2 → 4.10.3 へ、Spring Boot を 2.0.6 → 2.0.7 へバージョンアップする。。。が Spring Security の bug のため Spring Boot は 2.0.6 へ戻す ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回の記事において、org.springframework.security.authentication.ProviderManager#authenticate の中で getProviders() が DaoAuthenticationProvider を3つ返しているのを見つけたのですが、userDetailsService に ksbysample.webapp.lending.security.LendingUserDetailsService のインスタンスがセットされているものが2つありました(残りの1つは ImMemoryUserDetailsManager)。

      LendingUserDetailsService がセットされている DaoAuthenticationProvider が2つあると認証処理時に DB への検索処理が2回実行されるので、1つだけになるように変更します。

参照したサイト・書籍

目次

  1. WebSecurityConfig クラスから daoAuhthenticationProvider, configAuthentication メソッドをコメントアウトしてみる
  2. WebSecurityConfig クラスの configAuthentication メソッドで .userDetailsService(userDetailsService) をコメントアウトしてみる
  3. WebSecurityConfig クラスから daoAuhthenticationProvider メソッドだけコメントアウトしてみる
  4. まとめてみると。。。

手順

WebSecurityConfig クラスから daoAuhthenticationProvider, configAuthentication メソッドをコメントアウトしてみる

src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java から daoAuhthenticationProvider, configAuthentication メソッドをコメントアウトするとどうなるか確認してみます。

@Configuration
public class WebSecurityConfig {

    ..........

//    /**
//     * @return ???
//     */
//    @Bean
//    public AuthenticationProvider daoAuhthenticationProvider() {
//        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
//        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
//        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
//        return daoAuthenticationProvider;
//    }
//
//    /**
//     * @param auth ???
//     * @throws Exception
//     */
//    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
//    @Autowired
//    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
//        // AuthenticationManagerBuilder#userDetailsService の後に auth.inMemoryAuthentication() を呼び出すと
//        // AuthenticationManagerBuilder の defaultUserDetailsService に
//        // org.springframework.security.provisioning.InMemoryUserDetailsManager がセットされて
//        // Remember Me 認証で InMemoryUserDetailsManager が使用されて DB のユーザが参照されなくなるので、
//        // Remember Me 認証で使用する UserDetailsService を一番最後に呼び出す
//        // ※今回の場合には auth.userDetailsService(userDetailsService) が一番最後に呼び出されるようにする
//        auth.inMemoryAuthentication()
//                .withUser("actuator")
//                .password("{noop}xxxxxxxx")
//                .roles("ENDPOINT_ADMIN");
//        auth.authenticationProvider(daoAuhthenticationProvider())
//                .userDetailsService(userDetailsService);
//    }

}

「ログインを5回失敗すればアカウントはロックされる」のテストを debug 実行すると、org.springframework.security.authentication.ProviderManager#authenticate で getProviders() から userDetailsService に ksbysample.webapp.lending.security.LendingUserDetailsService のインスタンスがセットされている DaoAuthenticationProvider が1つ返ってきていました。特に何もしなくても DaoAuthenticationProvider がセットされるようです。

f:id:ksby:20190105141217p:plain f:id:ksby:20190105141335p:plain

テストも成功します。

f:id:ksby:20190105141955p:plain

ただし全てのテストを実行してみると「次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる」のテスト(Remember Me 認証のテスト)だけ失敗します。

f:id:ksby:20190105143321p:plain f:id:ksby:20190105143546p:plain

org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername の以下の場所でエラーになっており、delegate する先の UserDetailsService が見つからないことが原因のようです。

f:id:ksby:20190105143921p:plain

WebSecurityConfig クラスの configAuthentication メソッドで .userDetailsService(userDetailsService)コメントアウトしてみる

今度は src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java の configAuthentication メソッドで .userDetailsService(userDetailsService)コメントアウトしてみます。また passwordEncoder には BCryptPasswordEncoder のインスタンスをセットせず、デフォルトの DelegatingPasswordEncoder のインスタンスを使用します。

@Configuration
public class WebSecurityConfig {

    ..........

    /**
     * @return ???
     */
    @Bean
    public AuthenticationProvider daoAuhthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
//        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        return daoAuthenticationProvider;
    }

    /**
     * @param auth ???
     * @throws Exception
     */
    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        // AuthenticationManagerBuilder#userDetailsService の後に auth.inMemoryAuthentication() を呼び出すと
        // AuthenticationManagerBuilder の defaultUserDetailsService に
        // org.springframework.security.provisioning.InMemoryUserDetailsManager がセットされて
        // Remember Me 認証で InMemoryUserDetailsManager が使用されて DB のユーザが参照されなくなるので、
        // Remember Me 認証で使用する UserDetailsService を一番最後に呼び出す
        // ※今回の場合には auth.userDetailsService(userDetailsService) が一番最後に呼び出されるようにする
        auth.inMemoryAuthentication()
                .withUser("actuator")
                .password("{noop}xxxxxxxx")
                .roles("ENDPOINT_ADMIN");
        auth.authenticationProvider(daoAuhthenticationProvider());
//                .userDetailsService(userDetailsService);
    }

}

「ログインを5回失敗すればアカウントはロックされる」のテストを debug 実行すると、org.springframework.security.authentication.ProviderManager#authenticate で getProviders() から userDetailsService に ksbysample.webapp.lending.security.LendingUserDetailsService のインスタンスがセットされている DaoAuthenticationProvider と ImMemoryUserDetailsService のインスタンスがセットされている DaoAuthenticationProvider の計2つが返ってきていました。

f:id:ksby:20190105150501p:plain

テストは成功します。

f:id:ksby:20190105150247p:plain

全てのテストを実行してみると先程と同様に「次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる」のテスト(Remember Me 認証のテスト)だけ失敗します。ただし失敗の原因が異なり、今度は HTTPステータスコードが 200 ではなく 302 が返ってくるというものでした。

f:id:ksby:20190105151213p:plain f:id:ksby:20190105151333p:plain

これは Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その13 )( Remember Me 認証が使えなくなっていたので調査・修正する ) で調査済ですが、Remember Me 認証用の UserDetailsService として org.springframework.security.provisioning.InMemoryUserDetailsManager がセットされた後に別の UserDetailsService をセットしていないので、InMemoryUserDetailsManager が認証処理に使用されて認証エラーになり、ログイン画面へリダイレクトさせられているのでしょう。

WebSecurityConfig クラスから daoAuhthenticationProvider メソッドだけコメントアウトしてみる

今度は src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java から daoAuhthenticationProvider メソッドだけコメントアウトしてみます。

//    /**
//     * @return ???
//     */
//    @Bean
//    public AuthenticationProvider daoAuhthenticationProvider() {
//        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
//        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
//        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
//        return daoAuthenticationProvider;
//    }

    /**
     * @param auth ???
     * @throws Exception
     */
    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        // AuthenticationManagerBuilder#userDetailsService の後に auth.inMemoryAuthentication() を呼び出すと
        // AuthenticationManagerBuilder の defaultUserDetailsService に
        // org.springframework.security.provisioning.InMemoryUserDetailsManager がセットされて
        // Remember Me 認証で InMemoryUserDetailsManager が使用されて DB のユーザが参照されなくなるので、
        // Remember Me 認証で使用する UserDetailsService を一番最後に呼び出す
        // ※今回の場合には auth.userDetailsService(userDetailsService) が一番最後に呼び出されるようにする
        auth.inMemoryAuthentication()
                .withUser("actuator")
                .password("{noop}xxxxxxxx")
                .roles("ENDPOINT_ADMIN");
//        auth.authenticationProvider(daoAuhthenticationProvider())
//                .userDetailsService(userDetailsService);
        auth.userDetailsService(userDetailsService);
    }

「ログインを5回失敗すればアカウントはロックされる」のテストを debug 実行すると、org.springframework.security.authentication.ProviderManager#authenticate で getProviders() から userDetailsService に ksbysample.webapp.lending.security.LendingUserDetailsService のインスタンスがセットされている DaoAuthenticationProvider と ImMemoryUserDetailsService のインスタンスがセットされている DaoAuthenticationProvider の計2つが返ってきていました。

f:id:ksby:20190105152741p:plain

テストは成功します。

f:id:ksby:20190105152957p:plain

全てのテストを実行してみると、全て成功しました。

f:id:ksby:20190105153616p:plain

まとめてみると。。。

  • Remember Me 認証を使用しないのであれば、WebSecurityConfig クラスに daoAuhthenticationProvider, configAuthentication メソッドを定義する必要はない。
  • Remember Me 認証を使用するのであれば、WebSecurityConfig クラスに configAuthentication メソッドを定義して、その中で AuthenticationManagerBuilder#userDetailsService を呼び出して認証処理に使用する UserDetailsService クラスのインスタンスをセットする。
  • WebSecurityConfig クラスに configAuthentication メソッドを定義する場合、同じ UserDetailsService で AuthenticationManagerBuilder#authenticationProvider と AuthenticationManagerBuilder#userDetailsService を呼び出さないこと。認証処理時にセットした UserDetailsService で2回認証処理が行われることになる。

Remember Me 認証に対応しようとすると Spring Security は何気に難易度が上がる感じがします。

src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java は daoAuhthenticationProvider メソッドだけコメントアウトする案を採用し、コメントアウトではなく削除で反映します。

履歴

2019/01/05
初版発行。