理論編:パスワードを安全に保存しよう
パスワードをデータベースに保存するときは、平文で保存していはいけません。 平文とは、ユーザーの入力値そのままの文字列のことです。 パスワードを平文で保存すると、データベースがハッキングされたときにユーザーのパスワードが漏洩してしまいます。
このレクチャーでは、パスワードを安全に保存するための理論と実装方法を解説します。 まずは、パスワードを安全に保存するために必要な「ハッシュ」「ソルト」「ストレッチング」を理解するところからはじめましょう。
目次
ハッシュ
パスワードをデータベースに保存するときは、パスワードのハッシュ値を保存するようにします。 ハッシュ値とは、パスワードをハッシュ関数に入力したときに生成される値のことです。 ハッシュ関数は、 入力データから一定の固定長の値(ハッシュ値)を生成します。 たとえば、次のようなイメージです。
- 入力値:password
- 出力値:5f4dcc3b5aa765d61d8327deb882cf99
入力値の password という文字が、ハッシュ関数によって password とは異なる文字列に変換されています。
ハッシュ関数は非可逆変換です。 非可逆とは元に戻すことができないという意味です。 つまり、ハッシュ値から元の文字列を求めることは困難という性質があります。 この性質により、パスワードのハッシュ値をデータベースに保存すると、データベースに不正にアクセスされた場合でも攻撃者に元のパスワードを知られるリスクを軽減できます。
ソルト
ハッシュ化をするだけでは、パスワードの保護としては不十分です。 次のような理由が考えられます:
- ハッシュ化したとしても、パスワードの長さが短い場合は総当たり的に元のパスワードを計算できる場合がある
- 同じパスワードを使用しているユーザーがいると、ハッシュ値が同じになってしまう。攻撃者はハッシュ値が同じであるユーザーを見つけ出し、ハッシュ値から元のパスワードを解読できる
このような問題を解決するために、ソルト(salt)と呼ばれるランダムな文字列をパスワードと組み合わせてハッシュ化します。
- ユーザーが入力したパスワード:password
- ソルト:809a656af105f42401
- ハッシュ関数への入力値:809a656af105f42401password
ソルトによって、ハッシュ化するパスワードを長くできます。 また、ハッシュの計算の度に異なるソルトを使うことで同じパスワードでも異なるハッシュ値を生成できます。
ストレッチング
一般的に、ハッシュ値から元のパスワードを求めることは困難です。 しかし、総当り攻撃などによって、ハッシュ値から元のパスワードを求めることができる場合があります。
このような問題を解決するために、ストレッチングという処理を施します。 ストレッチングとは、ハッシュ化を何度も繰り返すことです。 ハッシュ化を何度も繰り返すと、ハッシュ値の解読に必要な計算量を増加させることができるため、総当たり攻撃のリスクを低減できます。
Spring Security でのハッシュ関数
Spring Security では、パスワードのハッシュ化を実現するクラスが複数提供されています。 これらのクラスは PasswordEncoder インターフェースを実装しています。 主な PasswordEncoder の実装クラスは次のようなクラスがあります:
- BCryptPasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
- Argon2PasswordEncoder
この講座では、BCryptPasswordEncoder を使用します。 BCryptPasswordEncoder は、BCrypt アルゴリズムを使用してパスワードをハッシュ化するクラスです。
BCryptPasswordEncoder によって生成されたハッシュ値を見てみましょう。
$2a$10$Uz3R17Fn4izSBDO15I5TpeEgb0ugusFqdHfxqic.7DbEMFzL8pW4O
この文字列は、$
を区切り文字として次のような構造になっています:
2a
:アルゴリズムのバージョン10
: コストパラメーター。ストレッチングの回数は 2^10 = 1024 回Uz3R17Fn4izSBDO15I5Tpe
:ソルトEgb0ugusFqdHfxqic.7DbEMFzL8pW4O
:ハッシュ値
パスワード認証時の処理
パスワード認証時には、次のような処理が行われます。
- データベースには、会員登録時などに登録されたパスワードのハッシュ値(*1)が保存されている
- ユーザーがログイン画面で入力したパスワードをサーバーが受け取る
- 入力されたパスワードをハッシュ化し、ハッシュ値(*2)を求める
- (*1) と (*2) が一致すれば認証成功、一致しなければ認証失敗
Spring Security の処理に PasswordEncoder を組み込む
Spring Security の処理に PasswordEncoder を組み込むには、次の2つの作業が必要です:
PasswordEncoder
インターフェースの実装クラスを Bean 登録する- ユーザー登録時に入力されたパスワードを encode してデータベースに保存する
PasswordEncoder
インターフェースの実装クラスを Bean 登録する
PasswordEncoder
インターフェースの実装クラスは、次のように Bean 登録します
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
このコードでは、@Bean
アノテーションを付与したメソッドによって BCryptPasswordEncoder
を Bean 登録しています。
PasswordEncoder を Bean 登録すると、フォーム認証時に PasswordEncoder
インターフェースの matches
メソッドを使用してパスワードが検証されます。
ユーザー登録時に入力されたパスワードを encode してデータベースに保存する
ユーザー作成時には PasswordEncoder
インターフェースの encode
メソッドを使用してパスワードをハッシュ化しデータベースに保存します。
import com.example.securedapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public void create(String username, String rawPassword) {
var encodedPassword = passwordEncoder.encode(rawPassword);
userRepository.insert(username, encodedPassword);
}
}
UserService.create
メソッドは会員登録処理で呼び出されるユーザーを作成するメソッドです。
メソッドの rawPassword
引数には、ユーザーの入力したパスワードが指定されます。
この rawPassword
を PasswordEncoder でハッシュ化し、データベースに保存している点が重要です。
passwordEncoder フィールドには、@Bean
で登録した BCryptPasswordEncoder
クラスを DI 経由で取得しています。
まとめ
このレクチャーでは、パスワードを安全に保存するための理論と実装方法を解説しました:
- パスワードを安全に保管するには、ハッシュ化、ソルト、ストレッチングが必要
- Spring Security では、パスワードのハッシュ化を実現するクラスが複数提供されている
- パスワード認証時には、入力されたパスワードをハッシュ化しハッシュ値を求め、データベースに保存されているハッシュ値と比較する