6.5 Password Hashing & UserDetailsService
← Back to Chapter 6: Spring Security & Authentication
Never store plaintext or reversibly encrypted passwords in a database. Secure authentication requires cryptographically salted, one-way adaptive hashing functions.
1. PasswordEncoder & BCrypt
Spring Security mandates the PasswordEncoder interface for credential hashing and matching:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) { return false; }
}
Why BCrypt is Industry Standard:
- Random Salt Built-In: Generates a cryptographically random 16-byte salt embedded into the hash string itself, making rainbow table attacks completely ineffective.
- Adaptive Work Factor (Cost Parameter): You can scale the hashing iterations (default
cost = 10, representing2^10 = 1024rounds) as hardware speeds up over time.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Cost factor of 12
}
A generated BCrypt hash:
$2a$12$e8uq5eL/4JjY8V6u.Q/Yce...
$2a$: BCrypt algorithm identifier$12$: Cost factor (2^12rounds)- Salt (22 chars) + Hash output (31 chars)
2. Implementing UserDetailsService & UserDetails
1. The Domain User Entity
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password; // Stored as BCrypt hash!
@Enumerated(EnumType.STRING)
private Role role;
private boolean enabled = true;
private boolean accountNonLocked = true;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
@Override
public String getUsername() { return email; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return accountNonLocked; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return enabled; }
}
2. Loading User From Database
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
}
}