Java

Spring Boot 2.0.0.RELEASE適用 → There is no PasswordEncoder mapped for the id “null”

会社の受発注システムで使っているSpring Bootを最新の2.0.0.RELESEにアップデートしました。これまでは2.0.0.M5。
テストケースは全部通って、本番環境にデプロイして問題なく動く(動いてませんでした)、けどなぜかローカルで動かしているときだけログイン時に例外が出ます。

2018-03-02 10:54:32,914 [http-nio-8080-exec-6] ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:238)
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:198)

There is no PasswordEncoder mapped for the id “null”」で検索したところStack OverFlowがヒットしました
maven – Spring Boot PasswordEncoder Error – Stack Overflow
createUser(User.withUsername(“user”).password(“user”).roles(“USER”).build()
としてユーザーを作っているところで
withDefaultPasswordEncoder().username(“user”).password(“user”).roles(“USER”).build();
とすれば良いという話。
つまり、これまでインメモリとはいえ生パスワードを格納しておくのはよろしくないよ、ということで明示的にパスワードを何かしらエンコードするストラテジーを指定しなければいけなくなったようです。

しかしながら、アプリのコードではビルダーは使わずにコンストラクタにプロパティを渡してパスワードを作っています。

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        String[] users = GlobalProp.getProperty("web.users").split(",");
        Properties usersProp = new Properties();
        for (String user : users) {
            usersProp.put(user, GlobalProp.getProperty("web." + user + ".password") + ",ROLE_USER,enabled");
        }
        return new InMemoryUserDetailsManager(usersProp);
    }

InMemoryUserDetailsManagerのコンストラクタ内でデフォルトパスワードエンコーダを使わせるヒントが必要そうな・・。
以下の記事ではNoOpPasswordEncoderというのを使う方法が紹介されています。
Spring Security 5 – There is no PasswordEncoder mapped for the id “null” – HarinathK.com

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

というブロックを追加することで動きました。本番で動いてローカルで動かなかったのは恐らく本番ではrememberMe authenticationが働いてログインをする必要がそもそもなかったからだと思われます。(動いてませんでした)

ちなみにNoOpPasswordEncoderはDeprecated。生パスワードをプロダクション環境でメモリ上にロードするのは危険だから、アプリ外でエンコードしておくべき、ということらしいです。LDAP認証かなんかに移行しようかな?
(本番環境はHTTPS接続で、パスワードは1Passwordで生成しためっちゃ長いのに指定するなどはしてあります)