Spring Securityでユーザー認証を実装するには、UserDetailsServiceとPasswordEncoderの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の設定#
UserDetailsServiceとPasswordEncoderを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"が発生する#
パスワードの検証に失敗しています。以下を確認してください。
- 登録時と認証時で同じ
PasswordEncoderを使用しているか
- データベースのパスワードカラムが十分な長さを持っているか(BCryptは60文字)
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機能の実装などを検討してください。
参考リンク#