Skip to content
Spring Boot sb security 3 min read

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.

StrengthRoundsApprox. timeUse
416< 1 msTests only
101024~50–100 msDefault, good general choice
124096~250 msHigher-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 PasswordEncoder bean — a delegating encoder requires the prefix, and BCryptPasswordEncoder writes 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/NoOpPasswordEncoder for passwords — they are fast and unsalted, exactly what you don’t want.
  • Don’t re-encode an already-encoded value; encode is 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.
Last updated June 13, 2026
Was this helpful?