Password Encoding
Passwords must never be stored in plaintext or with reversible encryption. Spring Security hashes them with a one-way, salted, deliberately slow algorithm and verifies a login by hashing the submitted password and comparing. The contract for this is the PasswordEncoder interface, and the recommended default implementation is BCrypt. This page covers the encoder, strength, the delegating encoder’s {bcrypt} prefix, and how encoding fits into registration.
The PasswordEncoder interface
PasswordEncoder has two methods that matter:
public interface PasswordEncoder {
String encode(CharSequence rawPassword); // hash on registration
boolean matches(CharSequence raw, String encoded); // verify on login
}
You never call matches yourself for login — DaoAuthenticationProvider does it. You do call encode when creating or changing a password.
BCryptPasswordEncoder
BCryptPasswordEncoder is the standard choice. BCrypt is adaptive (you tune its cost), salted automatically (every hash embeds a unique random salt), and slow by design, which makes brute-forcing expensive.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // default strength 10
}
A BCrypt hash is self-describing — algorithm, cost, salt, and digest are all in the string:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│ │ └ 22-char salt └ 31-char hash
│ └ cost factor (2^10 rounds)
└ algorithm version
Because the salt is stored inside the hash, the same password produces a different string every time you encode it — that is correct and expected.
Choosing a strength
The cost (log rounds) controls how slow hashing is. Higher is more secure but slower; pick the highest value your login latency budget tolerates.
| Strength | Rounds | Approx. time | Use |
|---|---|---|---|
| 4 | 16 | < 1 ms | Tests only |
| 10 | 1024 | ~50–100 ms | Default, good general choice |
| 12 | 4096 | ~250 ms | Higher-security apps |
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
Tip: Don’t go so high that login feels sluggish — hashing runs on every authentication. Strength 10–12 is the sweet spot for most web apps.
DelegatingPasswordEncoder and the {bcrypt} prefix
The factory PasswordEncoderFactories.createDelegatingPasswordEncoder() returns a DelegatingPasswordEncoder. Stored hashes are prefixed with the algorithm id in braces, so the encoder can pick the right algorithm per password and you can migrate algorithms over time without breaking existing logins.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
Encoding now yields a prefixed value:
{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
{argon2}$argon2id$v=19$m=16384,t=2,p=1$...$...
When verifying, the encoder reads the {id} prefix and delegates to the matching algorithm. Old {bcrypt} hashes keep working even after you switch the default to {argon2}.
Note: This is why in-memory and database user examples encode with the configured
PasswordEncoderbean — a delegating encoder requires the prefix, andBCryptPasswordEncoderwrites a bare hash. Be consistent between how you store and how you verify.
Encoding on registration
Always hash before saving. A registration service injects the encoder and calls encode:
@Service
@RequiredArgsConstructor
public class AccountService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public void register(String username, String rawPassword) {
UserAccount account = UserAccount.builder()
.username(username)
.password(passwordEncoder.encode(rawPassword)) // hash here
.roles(Set.of(Role.USER))
.build();
userRepository.save(account);
}
public void changePassword(UserAccount account, String newRaw) {
account.setPassword(passwordEncoder.encode(newRaw));
userRepository.save(account);
}
}
Quick experiment:
PasswordEncoder encoder = new BCryptPasswordEncoder();
String hash = encoder.encode("password");
System.out.println(hash);
System.out.println(encoder.matches("password", hash)); // true
System.out.println(encoder.matches("wrong", hash)); // false
Output:
$2a$10$7s5...redacted...K9
true
false
Pitfalls and rules
- Never store plaintext and never log raw passwords.
- Never use
MessageDigest/MD5/SHA-1/NoOpPasswordEncoderfor passwords — they are fast and unsalted, exactly what you don’t want. - Don’t re-encode an already-encoded value;
encodeis for raw input only. - If you mix a delegating encoder with bare hashes, logins fail with
There is no PasswordEncoder mapped for the id "null"— add the prefix or switch encoders consistently.