パスワード保存
Spring Security の PasswordEncoder インターフェースは、パスワードの一方向の変換を実行して、パスワードを安全に保存できるようにするために使用されます。PasswordEncoder は一方向の変換であるため、パスワード変換を双方向にする必要がある場合(データベースへの認証に使用される資格情報の保存など)には役立ちません。通常、PasswordEncoder は、認証時にユーザーが提供したパスワードと比較する必要があるパスワードを保存するために使用されます。
パスワード保存履歴
何年にもわたって、パスワードを保存するための標準的なメカニズムが進化してきました。当初、パスワードはプレーンテキストで保存されていました。データストアにアクセスするために必要な資格情報にパスワードが保存されているため、パスワードは安全であると見なされました。ただし、悪意のあるユーザーは、SQL インジェクションなどの攻撃を使用して、ユーザー名とパスワードの大規模な「データダンプ」を取得する方法を見つけることができました。ますます多くのユーザー資格情報が公開されるにつれて、セキュリティの専門家は、ユーザーのパスワードを保護するためにさらに多くのことを行う必要があることに気づきました。
その後、開発者は、SHA-256 などの一方向ハッシュを介してパスワードを実行した後にパスワードを保存するように促されました。ユーザーが認証を試みたとき、ハッシュされたパスワードは、ユーザーが入力したパスワードのハッシュと比較されます。これは、システムがパスワードの一方向のハッシュを保存するだけでよいことを意味しました。違反が発生した場合、パスワードの一方向ハッシュのみが公開されました。ハッシュは一方向であり、ハッシュが与えられたパスワードを推測することは計算上困難であったため、システム内の各パスワードを把握するために努力する価値はありません。この新しいシステムを打ち負かすために、悪意のあるユーザーはレインボーテーブル [Wikipedia] と呼ばれるルックアップテーブルを作成することにしました。毎回各パスワードを推測する作業を行うのではなく、パスワードを 1 回計算して、ルックアップテーブルに保存しました。
レインボーテーブルの効果を軽減するために、開発者はソルトされたパスワードを使用することが推奨されました。ハッシュ関数への入力としてパスワードだけを使用する代わりに、すべてのユーザーのパスワードに対してランダムなバイト(ソルトと呼ばれる)が生成されます。ソルトとユーザーのパスワードは、ハッシュ関数を介して実行され、一意のハッシュを生成します。ソルトは、ユーザーのパスワードと一緒にクリアテキストで保存されます。次に、ユーザーが認証を試みたときに、ハッシュされたパスワードが、保存されているソルトのハッシュおよびユーザーが入力したパスワードと比較されます。ユニークなソルトは、ソルトとパスワードの組み合わせごとにハッシュが異なるため、レインボーテーブルが効果的でなくなったことを意味します。
現代では、暗号化ハッシュ(SHA-256 など)はもはや安全ではないことがわかります。その理由は、最新のハードウェアでは、1 秒間に数十億のハッシュ計算を実行できるためです。これは、各パスワードを簡単に個別に解読できることを意味します。
開発者は現在、適応型一方向性関数を利用してパスワードを保存することが推奨されています。適応型一方向性関数を使用したパスワードの検証は、意図的にリソースを大量に消費します(意図的に大量の CPU、メモリ、その他のリソースを使用します)。適応型一方向性関数を使用すると、ハードウェアが向上するにつれて大きくなる可能性のある「作業要素」を構成できます。システムのパスワードを確認するために、「作業係数」を約 1 秒かかるように調整することをお勧めします。このトレードオフは、攻撃者がパスワードを解読することを困難にすることですが、システムに過度の負担をかけたり、ユーザーを苛立たせたりするほどのコストはかかりません。Spring Security は「作業係数」の良い出発点を提供しようとしましたが、パフォーマンスはシステムごとに大幅に異なるため、ユーザーは自分のシステムの「作業係数」をカスタマイズすることをお勧めします。使用する必要のある適応型一方向性関数の例には、bcrypt、PBKDF2、scrypt、および argon2 が含まれます。
アダプティブ一方向性関数は意図的にリソースを大量に消費するため、すべてのリクエストに対してユーザー名とパスワードを検証すると、アプリケーションのパフォーマンスが大幅に低下する可能性があります。検証リソースを集中的に使用することでセキュリティが確保されるため、Spring Security(または他のライブラリ)がパスワードの検証を高速化するためにできることは何もありません。ユーザーは、長期のクレデンシャル(つまり、ユーザー名とパスワード)を短期のクレデンシャル(セッション、OAuth トークンなど)と交換することをお勧めします。短期間のクレデンシャルは、セキュリティを損なうことなく迅速に検証できます。
DelegatingPasswordEncoder
Spring Security 5.0 より前のデフォルトの PasswordEncoder は NoOpPasswordEncoder であり、プレーンテキストのパスワードが必要でした。パスワード履歴セクションに基づいて、デフォルトの PasswordEncoder が BCryptPasswordEncoder のようになると予想する場合があります。ただし、これは 3 つの現実世界の問題を無視します。
多くのアプリケーションは、簡単に移行できない古いパスワードエンコーディングを使用しています。
パスワード保存のベストプラクティスは再び変更されます。
フレームワークとして、Spring Security は頻繁に重大な変更を加えることはできません。
代わりに、Spring Security は DelegatingPasswordEncoder を導入します。これは、次の方法ですべての問題を解決します。
現在のパスワードストレージの推奨事項を使用してパスワードがエンコードされていることを確認する
最新およびレガシー形式のパスワードの検証を許可する
将来的にエンコーディングをアップグレードできるようにする
PasswordEncoderFactories を使用すると、DelegatingPasswordEncoder のインスタンスを簡単に作成できます。
Java
Kotlin
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()または、独自のカスタムインスタンスを作成することもできます。
Java
Kotlin
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)パスワード保存形式
パスワードの一般的な形式は次のとおりです。
{id}encodedPasswordid は、使用する PasswordEncoder を検索するために使用される識別子であり、encodedPassword は、選択した PasswordEncoder の元のエンコードされたパスワードです。id は、パスワードの先頭にあり、{ で始まり、} で終わる必要があります。id が見つからない場合、id は null に設定されます。例: 以下は、さまざまな id 値を使用してエンコードされたパスワードのリストである可能性があります。元のパスワードはすべて password です。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)| 1 | 最初のパスワードの PasswordEncoder ID は bcrypt で、encodedPassword 値は $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG です。マッチングすると、BCryptPasswordEncoder に委譲されます |
| 2 | 2 番目のパスワードの PasswordEncoder ID は noop で、encodedPassword 値は password です。マッチングすると、NoOpPasswordEncoder に委譲されます |
| 3 | 3 番目のパスワードの PasswordEncoder ID は pbkdf2 で、encodedPassword 値は 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc です。マッチングすると、Pbkdf2PasswordEncoder に委譲されます |
| 4 | 4 番目のパスワードの PasswordEncoder ID は scrypt であり、encodedPassword 値は $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= です。一致すると、SCryptPasswordEncoder に委譲されます。 |
| 5 | 最終パスワードの PasswordEncoder ID は sha256 で、encodedPassword 値は 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 です。マッチングすると、StandardPasswordEncoder に委譲されます |
ユーザーの中には、潜在的なハッカーのために保存形式が提供されていることを懸念する人もいるかもしれません。パスワードの保存は、アルゴリズムが秘密であることに依存していないため、これは心配ありません。さらに、ほとんどの形式は、接頭辞がなくても攻撃者が容易に理解できるものです。例: BCrypt のパスワードは、しばしば |
パスワードエンコーディング
コンストラクターに渡された idForEncode は、パスワードのエンコードに使用される PasswordEncoder を決定します。以前に作成した DelegatingPasswordEncoder では、password のエンコード結果が BCryptPasswordEncoder に委譲され、接頭辞が {bcrypt} になることを意味します。最終結果は次の例のようになります。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BGパスワード照合
マッチングは、{id} と、コンストラクターで提供される id から PasswordEncoder へのマッピングに基づいています。パスワード保存形式の例は、これがどのように行われるかの実際の例を提供します。デフォルトでは、パスワードとマップされていない id (null ID を含む)を使用して matches(CharSequence, String) を呼び出した結果、IllegalArgumentException になります。この動作は、DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) を使用してカスタマイズできます。
id を使用することにより、任意のパスワードエンコーディングに一致させることができますが、最新のパスワードエンコーディングを使用してパスワードをエンコードします。暗号化とは異なり、パスワードハッシュはプレーンテキストを復元する簡単な方法がないように設計されているため、これは重要です。平文を復元する方法がないため、パスワードを移行することは困難です。ユーザーが NoOpPasswordEncoder を移行するのは簡単ですが、初心者向けのエクスペリエンスを簡単にするために、デフォルトで NoOpPasswordEncoder を含めることを選択しました。
はじめに
デモやサンプルをまとめる場合、ユーザーのパスワードをハッシュするのに時間がかかるのは少し面倒です。これを簡単にする便利なメカニズムがありますが、これはまだ本番用ではありません。
Java
Kotlin
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BGval user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG複数のユーザーを作成している場合は、ビルダーを再利用することもできます。
Java
Kotlin
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()これにより、保存されているパスワードがハッシュされますが、パスワードはメモリおよびコンパイルされたソースコードで公開されます。本番環境ではまだ安全とは見なされません。本番環境では、パスワードを外部でハッシュする必要があります。
Spring Boot CLI でエンコードする
パスワードを適切にエンコードする最も簡単な方法は、Spring Boot CLI を使用することです。
例: 次の例では、DelegatingPasswordEncoder で使用するために password のパスワードをエンコードしています。
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6トラブルシューティング
パスワード保存形式に従って、保存されているパスワードの 1 つに id がない場合、次のエラーが発生します。
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
これを解決する最も簡単な方法は、パスワードが現在どのように保存されているかを把握し、正しい PasswordEncoder を明示的に提供することです。
Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。
または、すべてのパスワードの前に正しい id を付けて、引き続き DelegatingPasswordEncoder を使用することもできます。例: BCrypt を使用している場合は、次のようなものからパスワードを移行します。
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
to
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG マッピングの完全なリストについては、PasswordEncoderFactories (Javadoc) の Javadoc を参照してください。
BCryptPasswordEncoder
BCryptPasswordEncoder 実装は、広くサポートされている bcrypt [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングに対する耐性を高めるために、bcrypt は意図的に低速になっています。他の適応型一方向関数と同様に、システムでパスワードを検証するのに約 1 秒かかるように調整する必要があります。BCryptPasswordEncoder (Javadoc) の Javadoc に記載されているように、BCryptPasswordEncoder のデフォルトの実装は強度 10 を使用します。パスワードの確認に約 1 秒かかるように、独自のシステムで強度パラメーターを調整してテストすることをお勧めします。
Java
Kotlin
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))Argon2PasswordEncoder
Argon2PasswordEncoder 実装は、Argon2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。Argon2 はパスワードハッシュコンペティション [Wikipedia] (英語) の勝者です。カスタムハードウェアでのパスワードクラッキングを無効にするために、Argon2 は、大量のメモリを必要とする意図的に低速なアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。Argon2PasswordEncoder の現在の実装には、BouncyCastle が必要です。
Java
Kotlin
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder 実装は、PBKDF2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングを打ち負かすには、PBKDF2 は意図的に遅いアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。このアルゴリズムは、FIPS 認定が必要な場合に適しています。
Java
Kotlin
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))SCryptPasswordEncoder
SCryptPasswordEncoder 実装は、scrypt [Wikipedia] (英語) アルゴリズムを使用してパスワードをハッシュします。カスタムハードウェアでのパスワードクラッキングを無効にするために、scrypt は、大量のメモリを必要とする意図的に低速なアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。
Java
Kotlin
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result)) その他の PasswordEncoder
下位互換性のために完全に存在する他の PasswordEncoder 実装が多数あります。これらはすべて非推奨であり、安全であるとは見なされなくなったことを示しています。ただし、既存のレガシーシステムを移行することは困難であるため、削除する予定はありません。
Password4j ベースのパスワードエンコーダ
Spring Security 7.0 は、パスワード 4j [GitHub] (英語) ライブラリをベースにした代替パスワードエンコーダ実装を導入します。これらのエンコーダは、一般的なハッシュアルゴリズムに追加のオプションを提供し、既存の Spring Security 実装の代替として使用できます。
Password4j ライブラリは、複数のアルゴリズムをサポートするパスワードハッシュに特化した Java 暗号化ライブラリです。これらのエンコーダーは、特定のアルゴリズム設定が必要な場合や、Password4j の最適化を活用したい場合に特に便利です。
すべての Password4j ベースのエンコーダーはスレッドセーフであり、複数のスレッド間で共有できます。
Argon2Password4jPasswordEncoder
Argon2Password4jPasswordEncoder 実装は、Password4j ライブラリを介して Argon2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。これは、Spring Security に組み込まれている Argon2PasswordEncoder とは異なる設定オプションと潜在的なパフォーマンス特性を備えた代替手段となります。
Argon2 はパスワードハッシュコンペティション [Wikipedia] (英語) に勝る暗号であり、新規アプリケーションに推奨されます。この実装は、出力ハッシュにソルトを適切に含める Password4j の Argon2 サポートを活用しています。
デフォルト設定でエンコーダーを作成します。
Java
Kotlin
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()カスタム Argon2 パラメーターを使用してエンコーダーを作成します。
Java
Kotlin
Argon2Function argon2Fn = Argon2Function.getInstance(65536, 3, 4, 32,
Argon2.ID);
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder(argon2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val argon2Fn = Argon2Function.getInstance(
65536, 3, 4, 32,
Argon2.ID
)
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder(argon2Fn)
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()BcryptPassword4jPasswordEncoder
BcryptPassword4jPasswordEncoder 実装は、Password4j ライブラリを介して BCrypt [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。これは、Password4j の実装特性を備えた、Spring Security 組み込みの BCryptPasswordEncoder の代替手段となります。
BCrypt は、ソルト生成機能を内蔵し、レインボーテーブル攻撃への耐性を持つ、定評のあるパスワードハッシュアルゴリズムです。この実装では、出力ハッシュにソルトを適切に含める Password4j の BCrypt サポートを活用しています。
デフォルト設定でエンコーダーを作成します。
Java
Kotlin
PasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val encoder: PasswordEncoder = BCryptPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()カスタム bcrypt パラメーターを使用してエンコーダーを作成します。
Java
Kotlin
BcryptFunction bcryptFn = BcryptFunction.getInstance(12);
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(bcryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val bcryptFunction = BcryptFunction.getInstance(12)
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder(bcryptFunction)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()ScryptPassword4jPasswordEncoder
ScryptPassword4jPasswordEncoder 実装は、Password4j ライブラリを介して SCrypt [Wikipedia] (英語) アルゴリズムを使用してパスワードをハッシュします。これは、Password4j の実装特性を備えた、Spring Security 組み込みの SCryptPasswordEncoder の代替手段となります。
SCrypt は、ハードウェアブルート強制攻撃への耐性を持つように設計された、メモリハードなパスワードハッシュアルゴリズムです。この実装では、出力ハッシュにソルトを適切に含める Password4j の SCrypt サポートを活用しています。
デフォルト設定でエンコーダーを作成します。
Java
Kotlin
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()カスタム scrypt パラメーターを使用してエンコーダーを作成します。
Java
Kotlin
ScryptFunction scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32);
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(scryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32)
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder(scryptFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()Pbkdf2Password4jPasswordEncoder
Pbkdf2Password4jPasswordEncoder 実装は、Password4j ライブラリを介して PBKDF2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。これは、明示的なソルト管理を備えた Spring Security 組み込みの Pbkdf2PasswordEncoder の代替手段となります。
PBKDF2 は、辞書攻撃やブルート強制攻撃を阻止するために、計算コストが高くなるように設計された鍵導出関数です。Password4j の PBKDF2 実装では出力ハッシュにソルトが含まれないため、この実装ではソルト管理を明示的に処理します。エンコードされたパスワード形式は {salt}:{hash} で、ソルトとハッシュはどちらも Base64 でエンコードされています。
デフォルト設定でエンコーダーを作成します。
Java
Kotlin
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()カスタム PBKDF2 パラメーターを使用してエンコーダーを作成します。
Java
Kotlin
PBKDF2Function pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256);
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(pbkdf2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256)
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder(pbkdf2Fn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()BalloonHashingPassword4jPasswordEncoder
BalloonHashingPassword4jPasswordEncoder 実装では、Password4j ライブラリを介してバルーンハッシュアルゴリズムを使用してパスワードをハッシュします。バルーンハッシュは、時間とメモリのトレードオフ攻撃とサイドチャネル攻撃の両方に耐性を持つように設計された、メモリハードなパスワードハッシュアルゴリズムです。
Password4j のバルーンハッシュ実装では出力ハッシュにソルトが含まれないため、この実装ではソルト管理を明示的に処理します。エンコードされたパスワード形式は {salt}:{hash} で、ソルトとハッシュはどちらも Base64 でエンコードされます。
デフォルト設定でエンコーダーを作成します。
Java
Kotlin
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()カスタムパラメーターを使用してエンコーダーを作成します。
Java
Kotlin
BalloonHashingFunction ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3);
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();val ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3)
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()パスワード保存設定
Spring Security はデフォルトで DelegatingPasswordEncoder を使用します。ただし、PasswordEncoder を Spring Bean として公開することにより、これをカスタマイズできます。
Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。
|
Java
XML
Kotlin
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}
XML 構成では、 |
パスワード設定の変更
ユーザーがパスワードを指定できるほとんどのアプリケーションには、そのパスワードを更新する機能も必要です。
パスワードを変更するためのよく知られた URL (英語) は、パスワードマネージャーが特定のアプリケーションのパスワード更新エンドポイントを検出できるメカニズムを示します。
この検出エンドポイントを提供するように Spring Security を構成できます。例: アプリケーションのパスワード変更エンドポイントが /change-password の場合、次のように Spring Security を構成できます。
Java
XML
Kotlin
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
passwordManagement { }
}
次に、パスワードマネージャーが /.well-known/change-password に移動すると、Spring Security はエンドポイント /change-password をリダイレクトします。
または、エンドポイントが /change-password 以外の場合は、次のように指定することもできます。
Java
XML
Kotlin
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management change-password-page="/update-password"/>
http {
passwordManagement {
changePasswordPage = "/update-password"
}
}
上記の構成では、パスワードマネージャーが /.well-known/change-password に移動すると、Spring Security は /update-password にリダイレクトされます。
侵害されたパスワードのチェック
パスワードが侵害されていないかどうかを確認する必要があるシナリオがいくつかあります。たとえば、機密データを扱うアプリケーションを作成している場合、信頼性を主張するためにユーザーのパスワードをチェックする必要があることがよくあります。これらのチェックの 1 つは、通常、データ侵害 [Wikipedia] (英語) でパスワードが見つかったためにパスワードが侵害されているかどうかを確認することです。
これを容易にするために、Spring Security は、CompromisedPasswordChecker (Javadoc) インターフェースの HaveIBeenPwnedRestApiPasswordChecker (Javadoc) 実装を介して Pwned API に遭遇しました (英語) との統合を提供します。
CompromisedPasswordChecker API を自分で使用することも、DaoAuthenticationProvider は Spring Security 認証メカニズム経由を使用している場合は CompromisedPasswordChecker Bean を提供することもできます。これは、Spring Security 構成によって自動的に取得されます。
そうすることで、弱いパスワード(たとえば 123456)を使用してフォームログイン経由で認証しようとすると、401 を受け取るか、/login?error ページにリダイレクトされます(ユーザーエージェントによって異なります)。ただし、その場合、401 またはリダイレクトだけではそれほど役に立ちません。ユーザーが正しいパスワードを入力したにもかかわらずログインできないため、混乱が生じます。このような場合、AuthenticationFailureHandler 経由で CompromisedPasswordException を処理して、ユーザーエージェントを /reset-password にリダイレクトするなど、必要なロジックを実行できます。例:
Java
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin((login) -> login
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
);
return http.build();
}
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
"/login?error");
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof CompromisedPasswordException) {
this.redirectStrategy.sendRedirect(request, response, "/reset-password");
return;
}
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
}
}@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
formLogin {
authenticationFailureHandler = CompromisedPasswordAuthenticationFailureHandler()
}
}
return http.build()
}
@Bean
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
return HaveIBeenPwnedRestApiPasswordChecker()
}
class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
private val redirectStrategy = DefaultRedirectStrategy()
override fun onAuthenticationFailure(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException
) {
if (exception is CompromisedPasswordException) {
redirectStrategy.sendRedirect(request, response, "/reset-password")
return
}
defaultFailureHandler.onAuthenticationFailure(request, response, exception)
}
}