こんにちは、かつコーチです。
前回の記事でUserDetailsServiceを実装しましたが、パスワードを平文のままデータベースに保存していませんか。
万が一データベースが漏えいした場合、平文パスワードはそのまま全ユーザーのアカウント情報の漏えいに直結します。
この記事では、Spring Securityの標準機能であるBCryptを使ったパスワードの安全な保存方法を解説します。
パスワードエンコーダとは何か
一方向にしか変換できないハッシュ化の仕組み
パスワードエンコーダとは、パスワードを不可逆な文字列(ハッシュ値)に変換する仕組みです。
不可逆というのは、ハッシュ値から元のパスワードを計算で復元できない、という意味です。
ログイン時には、入力されたパスワードを同じ方式でハッシュ化し、保存されているハッシュ値と一致するかどうかで照合します。
なぜ暗号化ではなくハッシュ化なのか
「暗号化」は鍵さえあれば元に戻せる処理ですが、「ハッシュ化」は元に戻すこと自体ができません。
パスワードは本来、サービス提供者側であっても知る必要がない情報です。
そのため、復元可能な暗号化ではなく、復元不可能なハッシュ化を使うのがパスワード管理の原則になっています。
実装手順
手順1:PasswordEncoderをBeanとして定義する
Spring SecurityはBCryptアルゴリズムを実装したBCryptPasswordEncoderを標準で提供しています。
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
BCryptはソルト(ランダムな文字列)を自動的に組み込む仕組みを持っており、同じパスワードでも毎回異なるハッシュ値が生成されます。
これにより、同じパスワードを使っている複数ユーザーのハッシュ値を比較して推測する攻撃(レインボーテーブル攻撃)を防げます。
手順2:ユーザー登録時にハッシュ化して保存する
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public void register(String email, String rawPassword) {
User user = new User();
user.setEmail(email);
user.setPasswordHash(passwordEncoder.encode(rawPassword));
userRepository.save(user);
}
}
passwordEncoder.encode()にパスワードを渡すだけで、ソルト付きのハッシュ値が生成されます。
手順3:ログイン時の照合はSpring Securityに任せる
前回の記事で実装したUserDetailsServiceがUserDetailsを返せば、パスワードの照合処理自体はSpring Securityが自動で行います。
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPasswordHash()) // ハッシュ化済みの値を渡す
.roles(user.getRole())
.build();
自前でpasswordEncoder.matches()を呼んで照合する必要はなく、DaoAuthenticationProviderが内部で自動的に処理してくれます。
つまずきやすい設定・注意点
password()にハッシュ化済みの値を渡す前提は、PasswordEncoderのBeanが正しく登録されている場合にのみ成立します。
筆者は開発初期、PasswordEncoderのBean定義を書き忘れたまま.encode()を呼び出し、NoSuchBeanDefinitionExceptionで起動すら失敗するというトラブルに遭遇したことがあります。
原因は単純で、@ConfigurationクラスにPasswordEncoderのBean定義を追加し忘れていたことでした。
依存性注入を使う場合、Beanの定義漏れがないかを起動エラーのメッセージから丁寧に追う癖をつけておくと、原因特定が早くなります。
よくあるつまずきポイント・エラー対処
❌ Before:平文のまま比較する
if (rawPassword.equals(user.getPasswordHash())) {
// ログイン成功
}
このコードは、passwordHashに平文のパスワードがそのまま保存されていることを前提にしており、致命的な脆弱性です。
✅ After:PasswordEncoderで照合する
if (passwordEncoder.matches(rawPassword, user.getPasswordHash())) {
// ログイン成功
}
matches()は、入力値を同じ方式でハッシュ化した上で、保存されているハッシュ値と比較してくれます。
Spring Securityの認証フローに乗せている場合はこのコードを自分で書く必要はありませんが、独自の照合ロジックを書く場面では必ずこの形にしてください。
応用・一歩先の使い方
ハッシュ化のコストパラメータを調整する
BCryptにはストレッチング回数(コストファクタ)というパラメータがあり、値が大きいほど計算に時間がかかり、総当たり攻撃への耐性が上がります。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // デフォルトは10
}
コストファクタを上げすぎるとログイン処理自体が遅くなるため、サーバーの性能とセキュリティ要件のバランスを見て決める必要があります。
複数のエンコード方式を共存させる
DelegatingPasswordEncoderを使うと、ハッシュ値の先頭に付く{bcrypt}のようなプレフィックスから、自動的に適切なエンコーダを選択できます。
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
古いアルゴリズムで保存された既存ユーザーのパスワードを、段階的にBCryptへ移行したい場合などに役立ちます。
まとめ
この記事のポイント
- パスワードは復元不可能な「ハッシュ化」で保存する
- Spring Securityの
BCryptPasswordEncoderはソルトを自動付与してくれる UserDetailsServiceが返すUserDetailsにハッシュ値を渡せば、照合はSpring Securityが自動で行う- コストファクタを調整することで、総当たり攻撃への耐性を高められる
次に読むべき記事
ここまででフォームログインの基本的な仕組みは一通り扱いました。
次はAPIで使われることが多いJWT認証の実装を見ていきましょう。
→ 次の記事:JWT認証を実装する
タグ: #SpringBoot #中級者向け #認証