Skip to main content

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, representing 2^10 = 1024 rounds) 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^12 rounds)
  • 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));
}
}