To hash a password in Java correctly, use an adaptive password-hashing function — bcrypt, or preferably Argon2id — with a unique per-password salt and a work factor tuned to your hardware. Do not use MessageDigest with SHA-256, SHA-512, or MD5, because those are built to be fast, and fast is precisely the wrong property for storing passwords. The distinction is not pedantic: a general-purpose hash lets an attacker with a leaked database try billions of guesses per second, while a properly tuned password hash caps them at thousands.
The reason comes down to intent. SHA-256 is designed to digest large inputs quickly. A password hash is designed to be deliberately, tunably slow, so that verifying one login costs a fraction of a second to your server but makes an offline cracking run economically painful. Salting defeats precomputed rainbow tables; the slow work factor defeats brute force.
Why plain SHA and MD5 fail
If you write MessageDigest.getInstance("SHA-256") over a password, three things go wrong. There is no salt, so identical passwords produce identical hashes and a rainbow table cracks them instantly. There is no work factor, so a modern GPU tries the entire common-password list in seconds. And MD5 in particular is cryptographically broken for collision resistance, though for password storage the speed is the fatal flaw regardless. Adding a salt to SHA-256 helps with rainbow tables but does nothing about speed. You need an algorithm that is slow by design.
Option 1: bcrypt
bcrypt has been the pragmatic default for years. It is well understood, has mature libraries, and its cost factor is a single number you increase over time. In Spring applications, Spring Security ships BCryptPasswordEncoder:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
var encoder = new BCryptPasswordEncoder(12); // strength (cost) factor
String hash = encoder.encode(rawPassword);
boolean ok = encoder.matches(rawPassword, hash);
The cost factor of 12 means 2^12 iterations. Each increment doubles the work. The output string embeds the algorithm, cost, and salt, so you store one column and matches reads everything it needs from it. One caveat: bcrypt only considers the first 72 bytes of input, so extremely long passphrases are silently truncated. If you support long passwords, pre-hash them or choose Argon2.
Without Spring, the at.favre.lib:bcrypt library gives you the same primitive:
import at.favre.lib.crypto.bcrypt.BCrypt;
String hash = BCrypt.withDefaults().hashToString(12, rawPassword.toCharArray());
BCrypt.Result r = BCrypt.verifyer().verify(rawPassword.toCharArray(), hash);
boolean ok = r.verified;
Option 2: Argon2id
Argon2 won the Password Hashing Competition and Argon2id is the variant recommended for password storage because it resists both GPU and side-channel attacks. It is memory-hard: you tune not just iterations but how much RAM each hash consumes, which is what makes it expensive to parallelize on specialized hardware. The de.mkammerer:argon2-jvm binding is the common Java choice:
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
char[] pw = rawPassword.toCharArray();
try {
// iterations, memory in KiB, parallelism
String hash = argon2.hash(3, 65536, 1, pw);
boolean ok = argon2.verify(hash, pw);
} finally {
argon2.wipeArray(pw); // clear the plaintext from memory
}
Note the char[] and the wipeArray call. Passwords as String linger in the string pool and cannot be zeroed; a char[] can be overwritten the moment you are done. It is a small habit that limits how long the plaintext sits in a heap dump.
Tuning the work factor
There is no universal number. The right cost is the largest one your infrastructure tolerates at peak login volume. A common target is roughly 250 milliseconds per hash on your production hardware. Benchmark it: measure encode at several cost factors under realistic load, pick the highest that keeps login latency acceptable, and revisit annually because hardware keeps getting faster. Store the parameters alongside the hash (both bcrypt and Argon2 encode them in the output string) so you can raise them later and rehash on next successful login.
Where this fits in a real application
Password hashing is one control. It does nothing if your crypto library itself carries a known vulnerability, or if a transitive dependency ships a flawed implementation — which is why dependency scanning belongs beside your code review. A tool such as Safeguard can flag a hashing or crypto library with a published advisory before it reaches production; our SCA overview explains how that transitive visibility works. For the broader picture of secure authentication design, the Academy has walkthroughs that put hashing in context with session handling and credential storage.
One more rule: never invent your own scheme, and never chain hashes hoping to add security. Use a vetted library at its recommended parameters. Cleverness in password storage almost always subtracts security.
FAQ
Is SHA-256 ever acceptable for passwords?
Not on its own. SHA-256 is fast by design, which lets attackers guess billions of candidates per second against a leaked database. Use bcrypt or Argon2id, which are deliberately slow and salted. SHA-256 is fine for integrity checks and non-password hashing.
bcrypt or Argon2 — which should I pick?
Argon2id is the stronger modern choice because it is memory-hard and resists GPU cracking better. bcrypt remains a solid, battle-tested option with mature Java support and simpler tuning. Either is defensible; avoid anything faster than these.
Do I still need a salt if I use bcrypt?
bcrypt and Argon2 generate and embed a unique salt automatically — you do not manage it separately. The salt is part of the output string, which is why two hashes of the same password look different.
How often should I increase the work factor?
Review it roughly once a year and whenever you upgrade hardware. Store the cost parameters in the hash so you can raise them and transparently rehash each user's password the next time they log in successfully.