Why does Spring Security require password encoding, and how does BCrypt work?
Quick Answer
Storing passwords in plaintext (or with weak/fast hashing like plain MD5/SHA-1) means a single database breach exposes every user's actual password; Spring Security's PasswordEncoder abstraction ensures passwords are always transformed before storage and compared via a matches() check rather than direct equality. BCrypt is the recommended default: it's a deliberately slow, adaptive hashing algorithm with a built-in random salt per password (preventing rainbow-table attacks) and a configurable work factor that can be increased over time as hardware gets faster, keeping brute-force attacks impractical even if the hashed values leak.
Detailed Answer
Storing passwords in plaintext means that any database breach (or even an overly-permissive internal query) directly exposes every user's actual password — a catastrophic outcome, especially since many users reuse passwords across services. Even naive hashing with a fast, general-purpose algorithm (plain MD5, SHA-1, or unsalted SHA-256) is inadequate: those algorithms are designed to be fast, which is exactly the wrong property for password storage — it makes brute-force/dictionary attacks against leaked hashes cheap, and without a per-password salt, identical passwords produce identical hashes, letting attackers use precomputed rainbow tables to reverse them in bulk.
Spring Security's PasswordEncoder interface abstracts password hashing so the application never handles or compares raw passwords directly:
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// registering a user
String hashed = passwordEncoder.encode(rawPassword); // store this, never the raw password
// checking a login attempt
boolean matches = passwordEncoder.matches(rawPasswordAttempt, hashed); // never compare hashes with ==
BCrypt, the long-standing recommended default, addresses both weaknesses of naive hashing:
- Built-in random salt: each call to
encode()generates a fresh random salt automatically and embeds it directly in the resulting hash string — so even two users with the identical password get completely different stored hash values, defeating rainbow-table attacks entirely. - Deliberately slow, and tunable ("adaptive"): BCrypt includes a configurable work factor (a "cost" parameter, typically defaulting to 10) controlling how computationally expensive each hash operation is. This is a deliberate, and very important, design choice: making each individual guess expensive is exactly what makes brute-forcing a leaked hash impractical at scale, even though it costs a small, one-time, imperceptible delay for a legitimate login. As hardware gets faster over time, the work factor can simply be increased for newly-hashed passwords, without needing to migrate every existing stored hash.
In practice: matches() re-derives the hash using the salt embedded in the stored value and compares the result — this is why you never need to store the salt separately, and why comparing two BCrypt hash strings directly with .equals() would be wrong; only passwordEncoder.matches(raw, stored) performs the comparison correctly.