Spring Security Fundamentals

Password Encoding

18 min Lesson 6 of 13

Password Encoding

Storing passwords as plaintext is one of the most destructive mistakes a developer can make. When a database is breached — and breaches happen to everyone — plaintext passwords give an attacker instant, complete access to every user account on your platform, often spilling across other services where users reused the same password. Spring Security's PasswordEncoder abstraction makes the right thing easy: you never see a raw password after registration, and the framework handles comparison safely for you.

The PasswordEncoder Contract

PasswordEncoder is a simple interface in org.springframework.security.crypto.password with three methods:

public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
  • encode() — hashes the raw password. Call this exactly once: when the user sets or changes their password. Never call it again for the same value.
  • matches() — takes the raw password from a login attempt and the stored hash, and returns true if they correspond. This is how Spring Security validates credentials.
  • upgradeEncoding() — signals whether a stored hash should be re-encoded with the current algorithm (used by DelegatingPasswordEncoder during phased migrations).
The encoder is stateless and thread-safe. Declare it as a Spring bean (singleton scope) and inject it wherever you need to hash passwords. There is no reason to create multiple instances.

Why BCrypt Is the Default Choice

Spring Security ships with several implementations — MD5, SHA-256, PBKDF2, SCrypt, Argon2, and BCrypt — but BCryptPasswordEncoder remains the practical default for most applications because:

  • Built-in salt: BCrypt generates a random 16-byte salt for every encode() call and embeds it in the output string. Two calls with the same raw password produce different hashes — there is no need to manage salts yourself, and rainbow tables are useless.
  • Configurable work factor: The strength parameter (default 10) is an exponent: the algorithm performs 2strength iterations. Increasing it by 1 doubles the time. As hardware gets faster you can raise the factor and re-hash on next login.
  • Fixed-length output: BCrypt always produces a 60-character string regardless of input length — safe to store in a VARCHAR(60) column.
  • Constant-time comparison: matches() does not short-circuit on the first differing byte, preventing timing attacks.

Registering BCryptPasswordEncoder as a Bean

Declare the encoder in a configuration class — typically alongside your SecurityFilterChain bean:

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 12 ≈ 250 ms on a modern server — good balance for 2024+ return new BCryptPasswordEncoder(12); } // ... SecurityFilterChain and UserDetailsService beans }
Choosing the right strength: Benchmark on your production hardware with BCryptPasswordEncoder.upgradeEncoding() or a simple loop. Aim for 150–300 ms per hash — slow enough to frustrate brute-force attempts, fast enough to not affect login UX. Strength 10 was fine in 2010; 12 is a sensible minimum today.

Encoding on Registration

Inject PasswordEncoder into the service that creates users, and encode before persisting:

import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepo; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepo, PasswordEncoder passwordEncoder) { this.userRepo = userRepo; this.passwordEncoder = passwordEncoder; } public User registerUser(String username, String rawPassword) { if (userRepo.existsByUsername(username)) { throw new UserAlreadyExistsException("Username taken: " + username); } User user = new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(rawPassword)); // <-- encode HERE user.setRole("ROLE_USER"); return userRepo.save(user); } }

After save() the raw password is out of scope. Only the BCrypt hash is persisted. If your service receives the raw password via a DTO, clear or discard the DTO after encoding.

How Spring Security Uses the Encoder at Login

When a user submits login credentials, Spring Security's DaoAuthenticationProvider calls userDetailsService.loadUserByUsername() to fetch the stored hash, then calls passwordEncoder.matches(submittedPassword, storedHash). You do not write this matching logic — the provider handles it automatically as long as your PasswordEncoder bean and your UserDetailsService bean are registered in the same application context.

// Spring Security does this internally — you do NOT call matches() yourself: boolean ok = passwordEncoder.matches(rawPasswordFromForm, user.getPassword()); if (!ok) throw new BadCredentialsException("Invalid credentials");
Never call encode() again at login time. encode() generates a new random salt on every call, so comparing two encoded versions of the same password will almost always return false. Always call matches(rawInput, storedHash) for verification.

DelegatingPasswordEncoder — Future-Proofing Your Hashes

Real applications eventually need to migrate from one algorithm to another without forcing all users to reset their passwords. Spring Security's DelegatingPasswordEncoder stores a prefix alongside each hash that identifies the algorithm used:

// Stored in the database: {bcrypt}$2a$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW // DelegatingPasswordEncoder reads the {bcrypt} prefix and routes // the matches() call to BCryptPasswordEncoder internally.

Create it via the factory method rather than by hand:

import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @Bean public PasswordEncoder passwordEncoder() { // Creates a DelegatingPasswordEncoder with bcrypt as the default return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }

This allows you to add a stronger algorithm later (e.g., Argon2) as the default for new registrations while still verifying old BCrypt hashes — upgradeEncoding() returns true for the old prefix so Spring re-hashes on successful login.

What About MD5 or SHA-256?

Both are general-purpose cryptographic hash functions, not password-hashing algorithms. They are designed to be fast — GPUs can compute billions of MD5 hashes per second. A modern GPU cluster can exhaust all common passwords in a leaked MD5 database in hours. BCrypt, SCrypt, and Argon2 are designed to be slow and memory-hard, making brute-force attacks economically impractical. Spring Security ships MessageDigestPasswordEncoder for MD5/SHA-256 but marks it @Deprecated — do not use it for new code.

Summary

Declare a BCryptPasswordEncoder bean (strength ≥ 12 for new projects), call encode() once at registration, let Spring Security's DaoAuthenticationProvider call matches() at login. Use DelegatingPasswordEncoder if you need a migration path between algorithms. Never store plaintext passwords, never use MD5 or SHA-256 for passwords, and never call encode() to verify a password. These rules are simple — the consequences of ignoring them are not.