Luồng chuẩn đi qua bốn mắt xích, mỗi mắt xích một nhiệm vụ:
1. Authentication filter (vd UsernamePasswordAuthenticationFilter) bóc username/password khỏi request, gói thành UsernamePasswordAuthenticationToken chưa xác thực rồi đưa cho AuthenticationManager.
2. AuthenticationManager (mặc định là ProviderManager) duyệt danh sách AuthenticationProvider để tìm cái xử lý được loại token này.
3. DaoAuthenticationProvider gọi UserDetailsService.loadUserByUsername() để lấy UserDetails (username, hash password, authorities, các cờ enabled/locked/expired).
4. Vẫn provider đó dùng PasswordEncoder.matches(raw, encoded) để đối chiếu mật khẩu. Khớp thì trả token đã xác thực kèm authorities; filter đặt nó vào SecurityContextHolder.
@Service
public class DbUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
AppUser user = repository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
return User.withUsername(user.getUsername())
.password(user.getPasswordHash())
.authorities(user.getRoles().toArray(new String[0]))
.disabled(!user.isActive())
.build();
}
}Điểm hay bị hỏi tiếp: UserDetailsService không so sánh password — nó chỉ trả dữ liệu, việc đối chiếu thuộc về PasswordEncoder trong provider.
Và khi username không tồn tại, provider vẫn chạy một lần encode giả để thời gian phản hồi không tiết lộ tài khoản nào có thật.