LoginSignup
5
3

More than 3 years have passed since last update.

Spring Security の DelegatingPasswordEncoder について調べた

Last updated at Posted at 2019-07-31

PasswordEncoder とは

Spring Security が提供するインターフェース。
パスワードを安全に保管するために、エンコードを行うメソッドと、エンコードされたパスワードと入力されたパスワードが一致しているかを確認するメソッドがある。
upgradeEncoding(...) というメソッドもあるが、これについては後述する。

DelegatingPasswordEncoder

Spring Security 5.0 から導入された PasswordEncoder の実装クラス。
これがデフォルトの PasswordEncoder となっている。このクラスは複数の PasswordEncoder に処理を委譲する。

なぜこのような実装が必要かというと、以下を見るといいと思う。
https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/html/overall-architecture.html#pe-history

端的にまとめると、

  • パスワードエンコード処理のベストプラクティスは変わっていっている。
  • 古いエンコードを利用しているシステムが多くあり、それをマイグレーションするのは簡単ではない。

ということらしい。

なので、複数の PasswordEncoder が共存できるように DelegatingPasswordEncoder ができたようだ。

エンコード文字列のフォーマット

複数の PasswordEncoder が共存しているため、どの実装クラスでエンコードされた文字列なのかを判定しないといけない。
(全てのエンコーダで検証すると時間がかかる。最近のエンコーダはわざと処理に時間がかかるようになっている。)

そのため、エンコードされた文字列に対して以下のように prefix を付与し、エンコーダを特定できるようにしている。

{bcrypt}$2a$10$7Y049iCPtPAGl6zyBSGoNeiTXbcr3NpPJ0xPEe2NmO7JDzLdGYv9K

prefix は DelegatingPasswordEncoder で指定することができる。(Map の key が prefix となる)

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

パスワードのエンコード

エンコードに利用する PasswordEncoderDelegatingPasswordEncoder のコンストラクタの第一引数で指定する。エンコード処理に利用する PasswordEncoder は必ず 1 種類になる。

上記の例であれば、BcryptPasswordEncoder がエンコードに利用される。

パスワードの検証

検証に利用する PasswordEncoder は prefix で指定されたものを利用する。prefix に指定された値と一致する PasswordEncoder が存在しない場合、デフォルトでは認証失敗となる。
DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatched(PasswordConder) メソッドを利用することで、一致しない場合に利用する PasswordEncoder を指定することもできる。

エンコードの更新が必要か判定する

エンコードの利用された PasswordEncoder が、現在使用している PasswordEncoder と異なるかどうか、upgradeEncoding(...) メソッドで判定することができる。
これは現在使用しているエンコーダに該当する prefix が付与されているかどうかを判定しているだけで、エンコード文字列を検証しているわけではない。

使ってみる

public class Main {

  public static void main(String[] args) {
    PasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();

    String idForEncode = "bcrypt";
    Map encoders = new HashMap<>();
    encoders.put(idForEncode, bCryptPasswordEncoder);
    encoders.put("pbkdf2", pbkdf2PasswordEncoder);

    DelegatingPasswordEncoder passwordEncoder =
            new DelegatingPasswordEncoder(idForEncode, encoders);

    // prefix に該当するものがない場合は BcryptPasswordEncoder を利用する
    passwordEncoder.setDefaultPasswordEncoderForMatches(bCryptPasswordEncoder);

    String s1 = passwordEncoder.encode("hogehoge");
    String s2 = bCryptPasswordEncoder.encode("hogehoge");
    String s3 = pbkdf2PasswordEncoder.encode("hogehoge");

    // {bcrypt} の prefix がついている
    System.out.println(s1);

    // prefix はついていない
    System.out.println(s2);
    System.out.println(s3);

    // prefix が {bcrypt} のため、BcryptPasswordEncoder で検証されている
    System.out.println(passwordEncoder.matches("hogehoge", s1));

    // prefix がついていないため、デフォルトである BcryptPasswordEncoder で検証されている
    System.out.println(passwordEncoder.matches("hogehoge", s2));
    System.out.println(passwordEncoder.matches("hogehoge", s3));

    // prefix が {pbkdf2} のため、Pbkdf2PasswordEncoder で検証されている
    System.out.println(passwordEncoder.matches("hogehoge", "{pbkdf2}" + s3));


    // {bcrypt} が付与されているのでアップグレードの必要はない
    System.out.println(passwordEncoder.upgradeEncoding(s1));

    // {bcrypt} が付与されていないのでアップグレードの必要あり
    System.out.println(passwordEncoder.upgradeEncoding(s2));
    System.out.println(passwordEncoder.upgradeEncoding(s3));
  }
}

出力は以下のようになる。

{bcrypt}$2a$10$iu6uhBTbICW7.Jk4C66a8O5lL7CYjJY3J5NfqRWPqzchLj9Q3KRrO
$2a$10$6URvwDoL1ebU73YcKd9FD.foyJHIvBFJPlGj/IjDX2emx7oIm.4jG
d7dbf38db5387f7e806dc1191ab23cde528ccae02d2459111027b0af6d0721c10476bdd5c106fc8e
true
true
false
true
false
true
true

移行を考える

Spring Security 4 系を利用していて、BcryptPasswordEncoder を利用していたが、
Spring Security 5 系へ移行し DelegatingPasswordEncoder を利用するようにしたい場合を考える。

エンコーダを変更しない場合

DelegatingPasswordEncoder の定義は以下のような感じ。

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

ここで問題になるのは、既存のエンコード文字列には prefix が付与されていないこと。
対応としては以下の 2 択か。

  • 保管されているエンコード文字列に prefix を付与する
  • DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatched(PasswordConder)BcryptPasswordEncoder を利用するように設定する

どちらにしてもそこまで難易度は高くないか。
エンコード文字列に prefix を付けると少し長くなるので、DB のカラム定義には気を付けないといけない。

エンコーダを変更する場合

例えば Pbkdf2PasswordEncoder に変更する場合。

String idForEncode = "pbkdf2";
Map encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put(idForEncode, new Pbkdf2PasswordEncoder());
PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

既存のエンコード文字列には prefix が付与されていないので、上記と同様の対処が必要。

で、bcrypt のものを pbkdf2 に変換するにはどうするか。
当然プレーンな文字列は保持していないため、システム側でどうすることもできない。
ユーザ側にプレーンなパスワードを入力してもらう必要がある。

1. ユーザが自発的にパスワードを更新するのを待つ

これで新しいエンコーダでエンコードされた文字列が保存される。
パスワードに期限が設定されているのであれば、自ずと変更される。
(パスワードに期限を設けるのは良くないと言われいるが・・・。)

2. ログイン時に更新を行う

ログイン時にはプレーンなパスワード文字列を入手することができるので、そのタイミングを利用する。
ログイン成功時に upgradeEncoding(...) メソッドで判定し、更新が必要であれば更新を行う。
パスワード変更を待つよりはましな気がする。

どちらにしても完ぺきに変更できるわけではないので、より強いエンコーダに変更するケースならこれでもいいような気がするけど、仮に今使っているエンコーダに脆弱性が見つかった場合はどうするべきなんだろうか?
某 pay みたいに全ユーザのパスワードを強制的に再発行するしかないのかな。

参考文献

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3