Use BCryptPasswordEncoder to hash passwords securely and understand why plaintext is never acceptable.
Published March 4, 2025
Passwords must never be stored in plaintext. Spring Security's PasswordEncoder abstraction makes it easy to hash passwords with modern algorithms.
// Register as a bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength/rounds (default 10)
// Each round increase doubles the work: 12 rounds ≈ 4× slower than 10
}
// Usage in registration
@Service
@RequiredArgsConstructor
public class UserService {
private final PasswordEncoder encoder;
private final UserRepository repo;
public User register(RegisterRequest req) {
String hash = encoder.encode(req.password());
// "$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
// ^ ^ ^
// | | salt (22 chars) + hash
// | rounds
// BCrypt version
return repo.save(new User(req.email(), hash));
}
public boolean checkPassword(String raw, String encoded) {
return encoder.matches(raw, encoded);
// Extracts salt from encoded, re-hashes raw, compares
}
}
Spring's default encoder since Spring Security 5: allows using different algorithms and migrating over time.
// Default: uses bcrypt, but can verify passwords encoded with other schemes
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// Stored as: {bcrypt}$2a$10$...
// Or: {sha256}...
// The prefix tells DelegatingPasswordEncoder which algorithm to use for verification
String encoded = encoder.encode("password"); // {bcrypt}$2a$10$...
encoder.matches("password", encoded); // true
@PostMapping("/api/v1/auth/register")
public ResponseEntity<Void> register(
@RequestBody @Valid RegisterRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new ConflictException("Email already registered");
}
userService.register(request);
return ResponseEntity.status(201).build();
}
record RegisterRequest(
@NotBlank @Email String email,
@NotBlank @Size(min = 8, max = 100) String password,
@NotBlank String name
) {}
// 1. Generate a secure token
String token = UUID.randomUUID().toString();
resetTokenRepo.save(new PasswordResetToken(userId, token,
Instant.now().plus(1, ChronoUnit.HOURS)));
// 2. Email the user a link with the token
// https://app.com/reset-password?token=<token>
// 3. On reset form submission
public void resetPassword(String token, String newPassword) {
PasswordResetToken prt = resetTokenRepo.findByToken(token)
.filter(t -> t.getExpiry().isAfter(Instant.now()))
.orElseThrow(() -> new InvalidTokenException("Token invalid or expired"));
userRepository.findById(prt.getUserId()).ifPresent(user -> {
user.setPasswordHash(encoder.encode(newPassword));
userRepository.save(user);
resetTokenRepo.delete(prt); // single-use
});
}
encoder.matches() uses constant-time comparison to prevent timing attacks — never use String.equals() to compare password hashes.