BCryptPasswordEncoder, strength tuning, and why you never store plaintext passwords.
Published September 21, 2026
Databases get breached, through SQL injection, a leaked backup or an insider. When that happens, what's stored in the password column decides whether attackers get nothing useful or every user's password. And because people reuse passwords, those passwords then unlock their email and bank accounts too.
The rule is simple: never store passwords, store a slow, salted hash of them. Spring Security's PasswordEncoder does this correctly, as long as you know which algorithm to pick and why.
SHA-256 and MD5 are designed to be fast. A modern GPU computes billions of them per second. An attacker with your hash table simply tries every common password and every variation, and most human-chosen passwords fall quickly.
Two defences are needed:
Password-hashing algorithms build both in: bcrypt, scrypt, Argon2 and PBKDF2. Argon2id is the current top recommendation (OWASP); bcrypt remains a solid, widely supported choice.
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // cost 12 → 2^12 = 4,096 rounds of the expensive key setup
}
String hash = encoder.encode("correct horse battery staple");
// $2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
// │ │ └── 22-character salt, then 31-character hash
// │ └── cost factor
// └── algorithm version
encoder.matches("correct horse battery staple", hash); // true
Everything needed to verify, including the algorithm, cost and salt, is inside the stored string. You don't store the salt separately. matches re-hashes the attempt using the same salt and cost, then compares.
The cost factor is exponential: each +1 doubles the time. Tune it so a hash takes roughly 100–500 ms on your production hardware, and raise it over the years as hardware gets faster. Remember that login endpoints become CPU-heavy under attack, so rate-limit them (see below).
BCrypt's 72-byte limit: bcrypt only uses the first 72 bytes of input. For typical passwords that's irrelevant, but it's a reason to cap password length sensibly (for example at 128 characters) and to prefer Argon2 for new systems where that matters.
DelegatingPasswordEncoder: upgrading algorithms over timeAlgorithms and cost factors change over a system's lifetime, and you can't rehash old passwords without the plaintext. Spring's default encoder solves this by prefixing each hash with its algorithm:
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
encoder.encode("secret"); // "{bcrypt}$2a$10$..."
encoder.matches("secret", "{sha256}..."); // verifies old hashes using the algorithm named in the prefix
New passwords use the current default, and old ones still verify. Implement UserDetailsPasswordService and Spring Security will re-hash a user's password with the current algorithm at their next successful login, which is the only moment you have the plaintext. That's how you migrate from SHA-256 to bcrypt, or from bcrypt to Argon2, without forcing resets.
Registration
public void register(RegisterRequest req) {
if (breachedPasswords.contains(req.password())) { // e.g. a check against known-breached lists
throw new WeakPasswordException("This password appears in known data breaches");
}
users.save(new User(req.email().toLowerCase(), encoder.encode(req.password())));
// a UNIQUE index on email is the real duplicate guard — an existsByEmail() check alone can race
}
Current guidance (NIST SP 800-63B) favours length (at least 8, ideally 12+) and blocking known-breached or common passwords over complex composition rules and forced periodic changes.
Login
matches() compares in constant time. Never compare hashes with String.equals yourself.Password reset
String token = generateSecureToken(); // e.g. 32 random bytes from SecureRandom, Base64url
resetTokens.save(new ResetToken(user.getId(), sha256(token), Instant.now().plus(Duration.ofMinutes(30))));
emailService.sendResetLink(user.getEmail(), "https://app.example.com/reset?token=" + token);
MessageDigest SHA-256 (even salted) instead of a password-hashing algorithm.Q: Why is bcrypt better than SHA-256 with a salt? A: The salt only defeats precomputed tables. SHA-256 is still extremely fast, so an attacker can try billions of guesses per second against each salted hash. Bcrypt is deliberately slow and tunable (the cost factor), which multiplies the attacker's work by orders of magnitude while one login stays fast enough for users.
Q: Where is the salt stored? A: Inside the bcrypt output string itself, together with the version and cost. The salt doesn't need to be secret, only unique and random per password. Its job is to force attackers to crack each hash individually.
Q: How would you migrate millions of users from an old SHA-1 scheme?
A: Two common approaches. First, verify with the old scheme at the next login, then re-hash with bcrypt (Spring's DelegatingPasswordEncoder plus UserDetailsPasswordService automate this). Second, to protect dormant accounts immediately, wrap the existing hashes: store bcrypt(sha1Hash) now, and verify with bcrypt(sha1(input)) until the user logs in and can be upgraded.
Q: What is a pepper? A: A secret value added to every password before hashing, stored outside the database (for example in a secrets manager or HSM). If only the database leaks, the hashes can't be attacked without it. It complements salting and a slow hash, and doesn't replace them.
Q: Argon2 or bcrypt?
A: Argon2id is the modern recommendation because it's memory-hard: it forces attackers to spend RAM as well as CPU, which blunts GPU and ASIC attacks. Bcrypt is still considered secure with an adequate cost factor and has wider support. Spring Security supports both (Argon2PasswordEncoder, BCryptPasswordEncoder).