Salva le password con password_hash($password, PASSWORD_DEFAULT) e verifica un login con password_verify($password, $hash), che restituisce true o false. Non salvare mai la password stessa, e non usare mai md5() o sha1() per questo.
Eseguilo due volte: l'hash è diverso ogni volta, ed entrambe le versioni vengono verificate. Lo stesso vale per ogni hash di questa pagina, quindi il tuo output non coinciderà con quello di nessun altro.
Login con password_verify
Un login verifica la password inviata con l'hash salvato quando è stato creato l'account. Questo blocco "registra" un utente e poi mostra un form di login; prova la password giusta (hunter2), poi una sbagliata.
Il messaggio di errore è lo stesso per un'email sconosciuta e per una password sbagliata. Dire quale delle due era sbagliata rivela a un attaccante quali email hanno un account. Dopo un login riuscito, un sito reale salva l'id utente nella sessione, come mostrato nella pagina sulle sessioni.
Cosa c'è dentro l'hash
Un hash bcrypt è un'unica stringa che contiene tutto ciò che serve a password_verify(): l'algoritmo, il cost e il salt, seguiti dall'hash vero e proprio. password_get_info() rilegge le prime parti.
Dato che il salt è salvato nell'hash, ti serve una sola colonna nel database, e due utenti con la stessa password ottengono comunque hash diversi. Rendi quella colonna VARCHAR(255): bcrypt è di 60 caratteri, ma PASSWORD_DEFAULT può passare a un algoritmo con un output più lungo.
Scegliere il cost
Il cost indica quanto è lento di proposito ogni hash, e ogni passo in più raddoppia il lavoro. PHP 8.3 usa cost 10 per bcrypt (PHP 8.4 ha alzato il default a 12). Un login dovrebbe richiedere circa da 100 a 300 ms di hashing; misuralo sul tuo server:
I numeri dipendono dalla macchina, ma ogni riga dovrebbe essere circa il doppio della precedente. Un valore più alto è più sicuro contro il cracking e più lento per ogni login, quindi scegli il cost più alto che la tua pagina di login può permettersi.
Aggiornare i vecchi hash con password_needs_rehash
Quando alzi il cost o PHP cambia PASSWORD_DEFAULT, gli hash esistenti continuano a funzionare, e password_needs_rehash() ti dice quali sono superati. L'unico momento in cui hai la password in chiaro è il login, quindi è lì che li aggiorni:
Il prefisso $2y$10$ diventa $2y$12$, e al login successivo password_needs_rehash() restituisce false.
Non usare mai md5 o sha1 per le password
md5() e sha1() sono hash generici veloci e senza salt. Veloce è proprio ciò che è sbagliato per le password: un attaccante con una tabella sottratta può provare miliardi di tentativi al secondo su una GPU, e l'hash senza salt di una password comune si trova già in tabelle di ricerca pubbliche. Confronta la velocità:
Nel tempo in cui password_hash() produce un hash, md5 ne produce centinaia di migliaia, quindi ogni tentativo di chi cerca di violarle costa quel numero di volte di meno. Cerca 5f4dcc3b5aa765d61d8327deb882cf99 e troverai subito la parola "password".
Se erediti una tabella di hash md5, non puoi convertirli direttamente. Incapsulali invece: salva password_hash($oldMd5) e verifica con password_verify(md5($input), $stored), poi sostituisci ciascuno con un hash normale quando quell'utente accede di nuovo.
Argon2id e il limite di 72 byte
PASSWORD_ARGON2ID usa più memoria per hash, il che rende molto più costoso il cracking con GPU. È disponibile quando PHP è compilato con libargon2 (lo sono la maggior parte dei pacchetti Linux); verificalo con defined('PASSWORD_ARGON2ID').
L'ultima riga stampa bool(true): due password diverse corrispondono perché bcrypt ignora tutto ciò che viene dopo il byte 72. Le password reali ci arrivano di rado, ma le passphrase in giapponese o in altri alfabeti multibyte raggiungono i 72 byte a circa 24 caratteri. Se consenti password molto lunghe, usa Argon2id, che non ha questo limite. password_verify() funziona con entrambi gli algoritmi, quindi puoi passare dall'uno all'altro con password_needs_rehash() come sopra.
Domande frequenti
Perché password_hash dà un risultato diverso ogni volta?
Ogni chiamata genera un nuovo salt casuale e lo salva dentro l'hash, quindi la stessa password dà ogni volta una stringa diversa. È voluto: password_verify() rilegge il salt dall'hash salvato, quindi restituisce comunque true per la password giusta.
Quanto deve essere lunga la colonna della password per password_hash?
Un hash bcrypt di PASSWORD_DEFAULT oggi è di 60 caratteri, ma l'algoritmo di default può cambiare in una futura versione di PHP. Usa VARCHAR(255) così un hash più lungo ci sta senza una migrazione.
Perché password_verify restituisce sempre false?
Le cause abituali sono un hash troncato (una colonna VARCHAR(50) taglia l'hash di 60 caratteri), passare come secondo argomento un nuovo password_hash() di qualcosa invece dell'hash salvato, confrontare con un hash creato con md5(), oppure spazi bianchi in più nel valore salvato. Verifica che strlen($hash) sia 60 e passa la password in chiaro come primo argomento.
Posso decifrare un valore di password_hash in PHP?
No. Un hash è a senso unico: non esiste una funzione che lo riporti alla password. Per verificare una password calcoli l'hash del tentativo e confronti, ed è ciò che fa password_verify(). Per una password dimenticata, invia un link di reimpostazione invece della vecchia password.
Meglio PASSWORD_DEFAULT, PASSWORD_BCRYPT o PASSWORD_ARGON2ID?
PASSWORD_DEFAULT è la scelta abituale: oggi è bcrypt e PHP potrebbe passarlo in futuro a un algoritmo più robusto, che password_needs_rehash() ti permette di adottare gradualmente. PASSWORD_ARGON2ID è più resistente al cracking con GPU e va benissimo quando la tua build di PHP lo include.