最新の安定バージョンについては、Spring Security 6.3.1 を使用してください! |
パスワード保存
Spring Security の PasswordEncoder
インターフェースは、パスワードの安全な保存を可能にするために、パスワードの一方向変換を実行するために使用されます。PasswordEncoder
は一方向の変換であるため、パスワード変換を双方向にする必要がある場合(つまり、データベースへの認証に使用される資格情報を保存する場合)は意図されていません。通常、PasswordEncoder
は、認証時にユーザーが指定したパスワードと比較する必要があるパスワードを保存するために使用されます。
パスワード保存履歴
パスワードを保存するための標準的な仕組みは、長年にわたって進化してきました。当初、パスワードはプレーンテキストで保存されていました。パスワードが保存されているデータストアにアクセスするには認証情報が必要なため、パスワードは安全であると考えられていました。しかし、悪意のあるユーザーは、SQL インジェクションなどの攻撃を用いて、ユーザー名とパスワードの大規模な「データダンプ」を取得する方法を見つけることができました。ユーザーの認証情報がどんどん公開されていく中で、セキュリティの専門家たちは、ユーザーのパスワードを保護するためにより多くのことを行う必要があることに気付きました。
開発者は、SHA-256 などの一方向ハッシュを介してパスワードを実行した後、パスワードを保存することが推奨されました。ユーザーが認証を試行すると、ハッシュされたパスワードは、入力したパスワードのハッシュと比較されます。つまり、システムはパスワードの一方向ハッシュを保存するだけで済みました。違反が発生した場合、パスワードの一方向ハッシュのみが公開されました。ハッシュは一方向であり、ハッシュが与えられたパスワードを推測することは計算上困難であったため、システム内の各パスワードを把握する努力は価値がありません。この新しいシステムを無効にするために、悪意のあるユーザーはレインボーテーブル [Wikipedia] として知られるルックアップテーブルを作成することにしました。各パスワードを毎回推測する作業を行うのではなく、パスワードを一度計算してルックアップテーブルに保存しました。
レインボーテーブルの有効性を緩和するために、開発者はソルトパスワードを使用することが推奨されました。ハッシュ関数への入力としてパスワードだけを使用する代わりに、ランダムなバイト(ソルトと呼ばれる)がすべてのユーザーのパスワードに対して生成されます。ソルトとユーザーのパスワードは、一意のハッシュを生成するハッシュ関数を介して実行されます。ソルトは、ユーザーのパスワードとともにクリアテキストで保存されます。次に、ユーザーが認証を試みると、ハッシュされたパスワードは、保存されたソルトのハッシュと入力したパスワードと比較されます。独自のソルトは、ソルトとパスワードの組み合わせごとにハッシュが異なるため、レインボーテーブルが効果的ではなくなったことを意味します。
現代では、暗号化ハッシュ(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", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
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()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
パスワード保存形式
パスワードの一般的な形式は次のとおりです。
{id}encodedPassword
id
は、どの 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 番目のパスワードには、noop の PasswordEncoder id と password の encodedPassword があります。一致すると、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 | 最終パスワードには、sha256 の PasswordEncoder id と 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 の encodedPassword があります。一致すると、StandardPasswordEncoder に委譲されます。 |
ユーザーの中には、潜在的なハッカーのために保存形式が提供されていることを懸念する人もいるかもしれません。パスワードの保存は、アルゴリズムがシークレットであることに依存していないため、これは心配ありません。さらに、ほとんどの形式は、接頭辞がなくても攻撃者が容易に理解できるものです。例: BCrypt のパスワードは、しばしば |
パスワードエンコーディング
コンストラクターに渡される idForEncode
は、パスワードのエンコードに使用される PasswordEncoder
を決定します。上記で作成した DelegatingPasswordEncoder
では、password
をエンコードした結果が BCryptPasswordEncoder
に委譲され、接頭辞として {bcrypt}
が付けられます。最終結果は次のようになります。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
パスワード照合
マッチングは、コンストラクターで提供される {id}
および id
から PasswordEncoder
へのマッピングに基づいて行われます。パスワード保存形式の例は、これがどのように行われるかの実例を提供します。デフォルトでは、パスワードとマッピングされていない id
(nullID を含む)で matches(CharSequence, String)
を呼び出した結果は IllegalArgumentException
になります。この動作は、DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
を使用してカスタマイズできます。
id
を使用することにより、任意のパスワードエンコーディングを照合できますが、最新のパスワードエンコーディングを使用してパスワードをエンコードします。暗号化とは異なり、パスワードハッシュはプレーンテキストを回復する簡単な方法がないように設計されているため、これは重要です。平文を復元する方法がないため、パスワードの移行が難しくなります。ユーザーが NoOpPasswordEncoder
を移行するのは簡単ですが、開始時の操作を簡単にするためにデフォルトで含めることを選択しました。
はじめに
デモやサンプルをまとめる場合、ユーザーのパスワードをハッシュするのに時間がかかるのは少し面倒です。これを簡単にする便利なメカニズムがありますが、これはまだ本番用ではありません。
Java
Kotlin
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val 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();
User user = users
.username("user")
.password("password")
.roles("USER")
.build();
User 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
を明示的に提供するように切り替えることです。これを解決する最も簡単な方法は、パスワードが現在どのように保存されているかを把握し、正しい 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
のデフォルト実装では、BCryptPasswordEncoder (Javadoc) の Javadoc で述べられているように、強度 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 = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder()
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 = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder()
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 = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
その他 PasswordEncoders
下位互換性のために完全に存在する他の PasswordEncoder
実装が多数あります。これらはすべて、もはや安全であると見なされないことを示すために非推奨です。ただし、既存のレガシーシステムを移行することは難しいため、削除する計画はありません。
パスワード保存設定
Spring Security はデフォルトで DelegatingPasswordEncoder を使用します。ただし、PasswordEncoder
を Spring Bean として公開することにより、これをカスタマイズできます。
Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder
Bean を公開することにより、以前の動作に戻すことができます。
|
Java
XML
Kotlin
@Bean
public static PasswordEncoder 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
にリダイレクトされます。