🔑 Spring Framework 講座【第29回】ユーザー認証の心臓部!〜UserDetailsServiceとPasswordEncoder〜

docs

前回、Spring Securityの基本導入と、アクセスルールの設定方法(認可)を学びました。しかし、設定ファイルを書いただけでは、実際にユーザーをログインさせる「認証」の仕組みは動きません。

今回は、Spring SecurityがユーザーのIDとパスワードを検証し、認証を完了させるために必須となる2つのコンポーネントについて、具体的な実装方法を学びます。

  1. UserDetailsService: ログインIDからユーザー情報を取得する(認証データプロバイダ)。
  2. PasswordEncoder: パスワードを安全にハッシュ化し、照合する(パスワードエンコーダー)。

1. ユーザー情報プロバイダ:UserDetailsService

Spring Securityは、ユーザーがログインフォームに入力したIDを受け取ると、そのIDに紐づくユーザー情報(ID、ハッシュ化されたパスワード、権限など)をDBから取得するために、UserDetailsServiceというインターフェースの実装クラスを呼び出します。

1-1. UserDetailsとUserDetailsService

  • UserDetailsService: ユーザー名を受け取り、UserDetailsオブジェクトを返すサービス(インターフェース)。
  • UserDetails: Spring Securityが認証・認可のために必要とするユーザー情報の入れ物(インターフェース)。DBの User Entityを直接返すのではなく、このインターフェースを実装したクラスを使います。

1-2. 実装例(UserService)

ここでは、既存のService層に UserDetailsService を実装する形で進めます。

Java

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

// UserDetailsServiceを実装
@Service 
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

    private final UserRepository userRepository; // ユーザー情報をDBから取得するRepository
    
    // ユーザー名(ログインID)を受け取り、UserDetailsを返すメソッド
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        // ① UserRepositoryを使ってDBからユーザー情報を取得
        User user = userRepository.findByUsername(username)
                                  .orElseThrow(() -> new UsernameNotFoundException("ユーザー名 " + username + " は見つかりません。"));
        
        // ② 取得したUser Entityから、UserDetailsオブジェクトを作成して返す
        return org.springframework.security.core.userdetails.User.builder()
                                // ユーザー名
                                .username(user.getUsername()) 
                                // ハッシュ化されたパスワード(後述)
                                .password(user.getPassword()) 
                                // ユーザーが持つ権限(Role)
                                .roles("USER") 
                                .build();
    }
}

これで、Spring SecurityはログインIDを受け取ると、この UserService を通じてDBからユーザー情報(特にハッシュ化されたパスワード)を取得できるようになります。


2. パスワードの安全な取り扱い:PasswordEncoder

セキュリティにおいて、パスワードを平文(暗号化されていないそのままの文字列)でデータベースに保存したり、ネットワークでやり取りしたりするのは絶対禁止です。

Spring Securityは、パスワードの安全な保管と照合のために、PasswordEncoderというインターフェースを定義しています。

2-1. ハッシュ化の原則

パスワードは、ハッシュ関数(例: BCrypt)を使って不可逆な文字列(ハッシュ値)に変換してDBに保存します。

  • 保存時: ユーザーが登録時に入力したパスワード $\to$ ハッシュ化 $\to$ DBに保存
  • 認証時: ユーザーがログイン時に入力したパスワード $\to$ ハッシュ化 $\to$ DBのハッシュ値と比較(一致すればOK)

2-2. PasswordEncoderのBean定義(BCryptPasswordEncoder)

Spring Securityでは、BCryptPasswordEncoder が推奨されています。このオブジェクトをBeanとして登録するだけで、Spring Securityが自動で利用してくれます。

Java

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 SecurityConfig {

    // ... (前回の SecurityFilterChain の定義など)

    // PasswordEncoder Beanの定義
    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCryptアルゴリズムを使用
        return new BCryptPasswordEncoder(); 
    }
}

この PasswordEncoder のBeanがコンテナに存在することで、Spring Securityはログイン時に以下の処理を自動で行います。

  1. ユーザーの入力パスワードをBCryptでハッシュ化。
  2. UserDetailsService から取得したDB保存済みのハッシュ値と照合。
  3. 一致すれば認証成功、失敗すれば認証失敗(例外スロー)。

3. DBへのパスワードの保存

上記の設定を有効にするには、DBに保存されているパスワードがBCryptでハッシュ化されている必要があります。

ユーザー登録のServiceメソッドなどで、パスワードを保存する前に必ず PasswordEncoder を使ってハッシュ化する処理を追加します。

Java

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
    
    // PasswordEncoderもDIで注入する
    private final PasswordEncoder passwordEncoder; 

    // 新規ユーザー登録メソッド
    @Transactional
    public User registerUser(String username, String rawPassword) {
        
        // パスワードをハッシュ化して保存する
        String encodedPassword = passwordEncoder.encode(rawPassword);
        
        User newUser = new User();
        newUser.setUsername(username);
        // ハッシュ化されたパスワードをセット
        newUser.setPassword(encodedPassword); 
        
        return userRepository.save(newUser);
    }
}

✅ 本日のまとめ

  • UserDetailsServiceは、ログインIDを受け取り、認証に必要なユーザー情報(UserDetails)をDBなどから取得する役割を担う。
  • PasswordEncoderは、パスワードを安全にハッシュ化し、ログイン時に入力されたパスワードとDBのハッシュ値を照合する役割を担う。
  • BCryptPasswordEncoder をBeanとして登録するのが、現在の標準的な実装である。
  • DBに保存するユーザーのパスワードは、必ず PasswordEncoder を使ってハッシュ化する必要がある。

🔔 次回予告

これで、ユーザーがログインフォームから認証する仕組みが完成しました。しかし、セキュリティ対策はこれだけでは不十分です。

次回は、認証後のセキュリティ、特にWebアプリケーションの古典的な脅威である**CSRF(クロスサイトリクエストフォージェリ)XSS(クロスサイトスクリプティング)**といった脆弱性への対策について学びます。

次回:【第30回】Webアプリケーションの主要な脅威とセキュリティ対策 にご期待ください!

コメント

タイトルとURLをコピーしました