PasswordEncoder and BCrypt
The PasswordEncoder Interface
public interface PasswordEncoder {
String encode(CharSequence rawPassword); // at registration
boolean matches(CharSequence rawPassword, String encodedPassword); // at login
default boolean upgradeEncoding(String encodedPassword) { return false; }
}encodecreates a new random salt each time, so the same password gives a different hash every call.matchesreads the salt from the stored hash, hashes the typed password the same way and compares in constant time.- You never decode a password. There is no
decodemethod.
Anatomy of a BCrypt Hash
Example
A BCrypt hash is one 60-character string that contains everything needed to verify it: version, cost and salt. That is why you need only one database column:
public class Main {
public static void main(String[] args) {
// What new BCryptPasswordEncoder().encode("asha123") stores (example value)
String hash = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";
String[] parts = hash.split("\\$"); // ["", "2a", "10", salt+hash]
String version = parts[1];
int cost = Integer.parseInt(parts[2]);
String salt = parts[3].substring(0, 22);
String digest = parts[3].substring(22);
System.out.println("length : " + hash.length());
System.out.println("version : " + version);
System.out.println("cost : " + cost + " -> 2^" + cost + " = " + (1 << cost) + " rounds");
System.out.println("salt : " + salt + " (22 chars, 128 bits)");
System.out.println("hash : " + digest + " (31 chars)");
System.out.println("cost 12 is " + ((1 << 12) / (1 << 10)) + "x slower than cost 10");
}
}Output
length : 60 version : 2a cost : 10 -> 2^10 = 1024 rounds salt : N9qo8uLOickgx2ZMRZoMye (22 chars, 128 bits) hash : IjZAgcfl7p92ldGxad68LJZdL17lhWy (31 chars) cost 12 is 4x slower than cost 10
Using BCrypt in Spring
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // cost 12: about 250 ms per hash on a typical server
}
// in a service
String hash = passwordEncoder.encode("asha123"); // $2a$12$...
boolean ok = passwordEncoder.matches("asha123", hash); // true| Cost | Rounds | Approx. time per hash |
|---|---|---|
| 10 (default) | 1,024 | ~60 ms |
| 12 | 4,096 | ~250 ms |
| 14 | 16,384 | ~1 s (too slow for most logins) |
Tip: Choose the highest cost that keeps a login under about 250–500 ms on your production servers. BCrypt only uses the first 72 bytes of a password; that is fine for normal passwords.
DelegatingPasswordEncoder
Algorithms age. PasswordEncoderFactories.createDelegatingPasswordEncoder() stores the algorithm id as a prefix, so old hashes keep working while new ones use the current default (BCrypt):
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{noop}plaintext <- only for tests, never in production@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}Note: A stored password without a {id} prefix makes the delegating encoder throw "There is no PasswordEncoder mapped for the id null". Either use the delegating encoder everywhere or a plain BCryptPasswordEncoder, consistently.