Spring Securityでユーザー認証を実装するには、UserDetailsServicePasswordEncoderの2つのコンポーネントが中心的な役割を果たします。本記事では、これらのインターフェースの役割を理解し、インメモリ認証からデータベース認証まで、安全なパスワード管理を実現する実装方法を解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Security 6.4.x
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の知識があると理解がスムーズです。

  • Spring Securityの基本概念(認証・認可の違い)
  • Spring Bootプロジェクトの基本的な構成
  • JPAを使用したデータベースアクセス(データベース認証を実装する場合)

認証処理の全体像

Spring Securityの認証処理では、UserDetailsServiceがユーザー情報を取得し、PasswordEncoderがパスワードの検証を行います。この2つのコンポーネントはDaoAuthenticationProviderによって協調動作します。

sequenceDiagram
    participant User as ユーザー
    participant Filter as UsernamePasswordAuthenticationFilter
    participant Provider as DaoAuthenticationProvider
    participant UDS as UserDetailsService
    participant PE as PasswordEncoder
    participant DB as データストア

    User->>Filter: 認証リクエスト(ID/パスワード)
    Filter->>Provider: 認証を委譲
    Provider->>UDS: loadUserByUsername(username)
    UDS->>DB: ユーザー情報を取得
    DB-->>UDS: UserDetails
    UDS-->>Provider: UserDetails
    Provider->>PE: matches(入力パスワード, ハッシュ)
    PE-->>Provider: true/false
    alt 認証成功
        Provider-->>Filter: Authentication
        Filter-->>User: 認証成功
    else 認証失敗
        Provider-->>Filter: AuthenticationException
        Filter-->>User: 401 Unauthorized
    end

この処理フローを理解したうえで、各コンポーネントの詳細を見ていきます。

UserDetailsインターフェースの理解

UserDetailsは、Spring Securityが認証に必要とするユーザー情報を表現するインターフェースです。認証処理において、このインターフェースを通じてユーザー名、パスワード、権限などの情報にアクセスします。

UserDetailsの主要メソッド

UserDetailsインターフェースは以下のメソッドを定義しています。

メソッド 戻り値 説明
getUsername() String ユーザー名を返す
getPassword() String ハッシュ化されたパスワードを返す
getAuthorities() Collection<? extends GrantedAuthority> 権限のコレクションを返す
isAccountNonExpired() boolean アカウントが有効期限内かどうか
isAccountNonLocked() boolean アカウントがロックされていないか
isCredentialsNonExpired() boolean 資格情報が有効期限内かどうか
isEnabled() boolean アカウントが有効かどうか

Userクラスによる簡易実装

Spring SecurityはUserDetailsの実装としてUserクラスを提供しています。多くの場合、このクラスをそのまま使用するか、ビルダーパターンで生成できます。

1
2
3
4
5
6
7
8
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

UserDetails user = User.builder()
    .username("user")
    .password("{bcrypt}$2a$10$...")
    .roles("USER")
    .build();

カスタムUserDetailsの実装

アプリケーション固有の情報(表示名、メールアドレスなど)を保持したい場合は、UserDetailsを実装したカスタムクラスを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class CustomUserDetails implements UserDetails {

    private final String username;
    private final String password;
    private final String displayName;
    private final String email;
    private final List<GrantedAuthority> authorities;
    private final boolean enabled;
    private final boolean accountNonLocked;

    public CustomUserDetails(String username, String password, String displayName,
                             String email, List<String> roles, boolean enabled,
                             boolean accountNonLocked) {
        this.username = username;
        this.password = password;
        this.displayName = displayName;
        this.email = email;
        this.authorities = roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .map(GrantedAuthority.class::cast)
            .toList();
        this.enabled = enabled;
        this.accountNonLocked = accountNonLocked;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    // アプリケーション固有のゲッター
    public String getDisplayName() {
        return displayName;
    }

    public String getEmail() {
        return email;
    }
}

UserDetailsServiceの実装

UserDetailsServiceは、ユーザー名からユーザー情報を取得するための単一メソッドを持つインターフェースです。

1
2
3
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

このインターフェースを実装することで、任意のデータソースからユーザー情報を取得できます。

インメモリ認証の実装

開発・テスト環境や簡易なアプリケーションでは、メモリ上にユーザー情報を保持するインメモリ認証が便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
            .roles("USER")
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
            .roles("USER", "ADMIN")
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

上記の例では、passwordの値がpasswordをBCryptでハッシュ化したものです。

データベース認証の実装

本番環境では、データベースからユーザー情報を取得する実装が一般的です。Spring Data JPAを使用した実装例を示します。

Entityクラスの定義

まず、ユーザー情報を格納するEntityクラスを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String displayName;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private boolean enabled = true;

    @Column(nullable = false)
    private boolean accountNonLocked = true;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set<String> roles = new HashSet<>();

    // コンストラクタ、ゲッター、セッター
    public UserEntity() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public String getDisplayName() { return displayName; }
    public void setDisplayName(String displayName) { this.displayName = displayName; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }

    public boolean isAccountNonLocked() { return accountNonLocked; }
    public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; }

    public Set<String> getRoles() { return roles; }
    public void setRoles(Set<String> roles) { this.roles = roles; }
}

Repositoryインターフェースの定義

1
2
3
4
5
6
7
8
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Optional<UserEntity> findByUsername(String username);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

カスタムUserDetailsServiceの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "ユーザーが見つかりません: " + username
            ));

        return new CustomUserDetails(
            user.getUsername(),
            user.getPassword(),
            user.getDisplayName(),
            user.getEmail(),
            user.getRoles().stream().toList(),
            user.isEnabled(),
            user.isAccountNonLocked()
        );
    }
}

@Transactional(readOnly = true)を付与することで、読み取り専用トランザクションとして最適化されます。また、UsernameNotFoundExceptionをスローすることで、Spring Securityが適切に認証失敗として処理します。

PasswordEncoderの設定と活用

PasswordEncoderは、パスワードのハッシュ化と検証を担当するインターフェースです。平文パスワードを保存することは重大なセキュリティリスクとなるため、必ずハッシュ化して保存する必要があります。

PasswordEncoderの主要メソッド

1
2
3
4
5
6
7
public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
メソッド 説明
encode() 平文パスワードをハッシュ化する
matches() 平文パスワードとハッシュ化されたパスワードを比較する
upgradeEncoding() エンコーディングのアップグレードが必要かどうかを判定する

Spring Securityが提供するPasswordEncoder実装

Spring Security 6.4では、以下のPasswordEncoder実装が提供されています。

実装クラス アルゴリズム 推奨度 特徴
BCryptPasswordEncoder BCrypt 推奨 適応型、ソルト自動生成
Argon2PasswordEncoder Argon2 推奨 メモリハード、最新
Pbkdf2PasswordEncoder PBKDF2 推奨 FIPS準拠が必要な場合
SCryptPasswordEncoder SCrypt 推奨 メモリハード
NoOpPasswordEncoder なし 非推奨 テスト用のみ

BCryptPasswordEncoderの使用

BCryptは、最も広く使用されているパスワードハッシュアルゴリズムです。ソルトを自動生成し、ストレッチング(繰り返しハッシュ計算)により計算コストを調整できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // strength: 4〜31の範囲で指定(デフォルト: 10)
        // 値が大きいほど計算コストが増加(2^strength回の計算)
        return new BCryptPasswordEncoder(12);
    }
}

strengthパラメータは、計算コストを調整するための値です。システムの性能に応じて、パスワード検証に約1秒かかるように調整することが推奨されています。

DelegatingPasswordEncoderの活用

DelegatingPasswordEncoderは、複数のエンコーディング形式を扱えるラッパーです。レガシーシステムからの移行や、将来のアルゴリズム変更に対応できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // デフォルトでBCryptを使用し、他の形式もサポート
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

DelegatingPasswordEncoderを使用する場合、パスワードは{id}encodedPasswordの形式で保存されます。

形式 説明
{bcrypt}$2a$10$... BCryptでエンコード
{argon2}$argon2id$... Argon2でエンコード
{pbkdf2}... PBKDF2でエンコード
{noop}password 平文(テスト用のみ)

カスタムDelegatingPasswordEncoderの作成

特定のアルゴリズムのみをサポートしたい場合は、カスタムで作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        String defaultEncoder = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder(12));
        encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());

        return new DelegatingPasswordEncoder(defaultEncoder, encoders);
    }
}

SecurityFilterChainの設定

UserDetailsServicePasswordEncoderをSpring Securityの設定に組み込みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService,
            PasswordEncoder passwordEncoder) {

        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);

        return new ProviderManager(authProvider);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/register", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );

        return http.build();
    }
}

ユーザー登録機能の実装

認証機能と合わせて、新規ユーザーを登録する機能も実装します。パスワードは登録時にハッシュ化して保存します。

ユーザー登録サービス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    public UserEntity registerUser(String username, String rawPassword,
                                   String displayName, String email) {
        // 重複チェック
        if (userRepository.existsByUsername(username)) {
            throw new IllegalArgumentException("ユーザー名は既に使用されています");
        }
        if (userRepository.existsByEmail(email)) {
            throw new IllegalArgumentException("メールアドレスは既に使用されています");
        }

        UserEntity user = new UserEntity();
        user.setUsername(username);
        // パスワードをハッシュ化して保存
        user.setPassword(passwordEncoder.encode(rawPassword));
        user.setDisplayName(displayName);
        user.setEmail(email);
        user.setRoles(Set.of("USER"));
        user.setEnabled(true);
        user.setAccountNonLocked(true);

        return userRepository.save(user);
    }

    @Transactional
    public void changePassword(String username, String currentPassword, String newPassword) {
        UserEntity user = userRepository.findByUsername(username)
            .orElseThrow(() -> new IllegalArgumentException("ユーザーが見つかりません"));

        // 現在のパスワードを検証
        if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
            throw new IllegalArgumentException("現在のパスワードが正しくありません");
        }

        // 新しいパスワードをハッシュ化して保存
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
    }
}

ユーザー登録コントローラー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class RegistrationController {

    private final UserService userService;

    public RegistrationController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/register")
    public String showRegistrationForm() {
        return "register";
    }

    @PostMapping("/register")
    public String registerUser(
            @RequestParam String username,
            @RequestParam String password,
            @RequestParam String displayName,
            @RequestParam String email,
            RedirectAttributes redirectAttributes) {
        try {
            userService.registerUser(username, password, displayName, email);
            redirectAttributes.addFlashAttribute("message", "登録が完了しました");
            return "redirect:/login";
        } catch (IllegalArgumentException e) {
            redirectAttributes.addFlashAttribute("error", e.getMessage());
            return "redirect:/register";
        }
    }
}

パスワードセキュリティのベストプラクティス

安全なパスワード管理を実現するためのベストプラクティスをまとめます。

パスワードポリシーの実装

Bean Validationを使用して、パスワードの複雑性を検証するカスタムバリデータを実装できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;

import java.lang.annotation.*;
import java.util.regex.Pattern;

@Documented
@Constraint(validatedBy = StrongPasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface StrongPassword {
    String message() default "パスワードは8文字以上で、大文字、小文字、数字を含む必要があります";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {

    // 8文字以上、大文字・小文字・数字を含む
    private static final Pattern PASSWORD_PATTERN = Pattern.compile(
        "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"
    );

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if (password == null) {
            return false;
        }
        return PASSWORD_PATTERN.matcher(password).matches();
    }
}

漏洩パスワードチェック

Spring Security 6.4では、Have I Been Pwned APIと連携して、漏洩したパスワードかどうかをチェックできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker;

@Configuration
public class PasswordSecurityConfig {

    @Bean
    public CompromisedPasswordChecker compromisedPasswordChecker() {
        return new HaveIBeenPwnedRestApiPasswordChecker();
    }
}

セキュリティ対策のチェックリスト

対策 説明 実装状況
適応型ハッシュ関数の使用 BCrypt、Argon2などを使用 必須
十分なストレッチング 検証に約1秒かかるよう調整 推奨
パスワード複雑性要件 最小長、文字種の要件を設定 推奨
漏洩パスワードチェック 既知の漏洩パスワードを拒否 推奨
アカウントロック 連続失敗時にロック 推奨
パスワード履歴 過去のパスワードの再利用を禁止 任意

認証情報へのアクセス

認証後、コントローラーやサービスで現在のユーザー情報にアクセスする方法を示します。

SecurityContextHolderを使用する方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityUtils {

    public static CustomUserDetails getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
            return (CustomUserDetails) authentication.getPrincipal();
        }
        return null;
    }
}

@AuthenticationPrincipalアノテーションを使用する方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProfileController {

    @GetMapping("/api/profile")
    public ProfileResponse getProfile(@AuthenticationPrincipal CustomUserDetails user) {
        return new ProfileResponse(
            user.getUsername(),
            user.getDisplayName(),
            user.getEmail()
        );
    }
}

record ProfileResponse(String username, String displayName, String email) {}

トラブルシューティング

よくあるエラーと対処法

“There is no PasswordEncoder mapped for the id null”

パスワードにID接頭辞({bcrypt}など)がない場合に発生します。

1
2
3
4
5
6
7
// 対処法1: パスワードに接頭辞を追加
// {bcrypt}$2a$10$...

// 対処法2: デフォルトエンコーダーを設定
DelegatingPasswordEncoder delegatingEncoder = 
    (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
delegatingEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());

“Bad credentials"が発生する

パスワードの検証に失敗しています。以下を確認してください。

  1. 登録時と認証時で同じPasswordEncoderを使用しているか
  2. データベースのパスワードカラムが十分な長さを持っているか(BCryptは60文字)
  3. UserDetailsServiceが正しくパスワードを返しているか

ユーザーが見つからない

UsernameNotFoundExceptionがスローされています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ログを追加してデバッグ
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    log.debug("ユーザー検索: {}", username);
    return userRepository.findByUsername(username)
        .orElseThrow(() -> {
            log.warn("ユーザーが見つかりません: {}", username);
            return new UsernameNotFoundException("ユーザーが見つかりません: " + username);
        });
}

まとめ

本記事では、Spring Securityにおけるユーザー認証の実装方法を解説しました。

  • UserDetailsはユーザー情報を表現するインターフェースで、Userクラスまたはカスタム実装を使用できる
  • UserDetailsServiceはユーザー名からユーザー情報を取得する役割を担い、インメモリやデータベースから実装できる
  • PasswordEncoderはパスワードのハッシュ化と検証を行い、BCryptが最も一般的な選択肢となる
  • DelegatingPasswordEncoderを使用することで、複数のエンコーディング形式に対応できる
  • パスワードセキュリティには、適応型ハッシュ関数、パスワードポリシー、漏洩チェックなど複合的な対策が重要

次のステップとして、JWT認証やOAuth2/OpenID Connectによるソーシャルログイン、Remember-Me機能の実装などを検討してください。

参考リンク