Przechowuj hasła przez password_hash($password, PASSWORD_DEFAULT), a logowanie sprawdzaj przez password_verify($password, $hash), które zwraca true albo false. Nigdy nie przechowuj samego hasła i nigdy nie używaj do niego md5() ani sha1().
Uruchom to dwa razy: hash za każdym razem jest inny, a obie wersje przechodzą weryfikację. To samo dotyczy każdego hasha na tej stronie, więc twój wynik nie będzie zgodny z niczyim innym.
Logowanie przez password_verify
Logowanie sprawdza wysłane hasło z hashem zapisanym przy tworzeniu konta. Ten blok „rejestruje” jednego użytkownika, a potem pokazuje formularz logowania; spróbuj właściwego hasła (hunter2), a potem złego.
Komunikat błędu jest taki sam dla nieznanego e-maila i złego hasła. Powiedzenie, co było źle, zdradza atakującemu, które e-maile mają konta. Po udanym logowaniu prawdziwa strona zapisuje identyfikator użytkownika w sesji, jak pokazuje strona o sesjach.
Co jest w hashu
Hash bcrypt to jeden string zawierający wszystko, czego potrzebuje password_verify(): algorytm, koszt i sól, a po nich sam hash. password_get_info() odczytuje pierwsze części z powrotem.
Ponieważ sól jest zapisana w hashu, potrzebujesz tylko jednej kolumny w bazie danych, a dwóch użytkowników z tym samym hasłem i tak dostaje różne hashe. Niech ta kolumna ma VARCHAR(255): bcrypt ma 60 znaków, ale PASSWORD_DEFAULT może zmienić się na algorytm z dłuższym wynikiem.
Wybór kosztu
Koszt określa, jak celowo wolne jest każde haszowanie, a każdy krok w górę podwaja pracę. PHP 8.3 używa dla bcrypt kosztu 10 (PHP 8.4 podniosło domyślną wartość do 12). Logowanie powinno zajmować mniej więcej od 100 do 300 ms haszowania; zmierz to na własnym serwerze:
Liczby zależą od maszyny, ale każda linia powinna być mniej więcej dwa razy większa od poprzedniej. Wyższy koszt jest bezpieczniejszy wobec łamania i wolniejszy przy każdym logowaniu, więc wybierz najwyższy, na jaki stać twoją stronę logowania.
Aktualizacja starych hashy przez password_needs_rehash
Gdy podnosisz koszt albo PHP zmienia PASSWORD_DEFAULT, istniejące hashe nadal działają, a password_needs_rehash() mówi, które są nieaktualne. Jedyna chwila, w której masz czyste hasło, to logowanie, więc tam aktualizujesz:
Prefiks $2y$10$ zmienia się na $2y$12$, a przy następnym logowaniu password_needs_rehash() zwraca false.
Nigdy nie używaj md5 ani sha1 do haseł
md5() i sha1() to szybkie hashe ogólnego przeznaczenia bez soli. Szybkość to przy hasłach dokładnie to, czego nie chcesz: atakujący z wykradzioną tabelą może sprawdzać miliardy prób na sekundę na GPU, a niesolony hash popularnego hasła jest już w publicznych tablicach wyszukiwania. Porównaj szybkość:
W czasie, w którym password_hash() tworzy jeden hash, md5 tworzy ich setki tysięcy, więc każda próba łamiącego jest tyle razy tańsza. Wyszukaj 5f4dcc3b5aa765d61d8327deb882cf99, a od razu znajdziesz słowo „password”.
Jeśli odziedziczysz tabelę hashy md5, nie da się ich bezpośrednio przekonwertować. Zamiast tego je opakuj: zapisz password_hash($oldMd5) i weryfikuj przez password_verify(md5($input), $stored), a potem zastępuj każdy zwykłym hashem przy następnym logowaniu danego użytkownika.
Argon2id i limit 72 bajtów
PASSWORD_ARGON2ID zużywa więcej pamięci na hash, co czyni łamanie przez GPU dużo droższym. Jest dostępny, gdy PHP zbudowano z libargon2 (tak jest w większości pakietów dla Linuksa); sprawdź przez defined('PASSWORD_ARGON2ID').
Ostatnia linia wypisuje bool(true): dwa różne hasła pasują, bo bcrypt ignoruje wszystko po 72. bajcie. Prawdziwe hasła rzadko tyle osiągają, ale frazy po japońsku lub w innych pismach wielobajtowych dochodzą do 72 bajtów przy około 24 znakach (polskie litery z ogonkami też zajmują po 2 bajty). Jeśli pozwalasz na bardzo długie hasła, użyj Argon2id, który nie ma takiego limitu. password_verify() działa z oboma algorytmami, więc możesz przejść na inny przez password_needs_rehash(), jak wyżej.
Najczęściej zadawane pytania
Dlaczego password_hash za każdym razem daje inny wynik?
Każde wywołanie generuje nową losową sól i zapisuje ją wewnątrz hasha, więc to samo hasło daje za każdym razem inny string. Tak ma być: password_verify() odczytuje sól z zapisanego hasha, więc dla właściwego hasła nadal zwraca true.
Jak długa powinna być kolumna na hasło z password_hash?
Hash bcrypt z PASSWORD_DEFAULT ma dziś 60 znaków, ale domyślny algorytm może się zmienić w przyszłej wersji PHP. Użyj VARCHAR(255), żeby dłuższy hash zmieścił się bez migracji.
Dlaczego password_verify zawsze zwraca false?
Zwykłe przyczyny to obcięty hash (kolumna VARCHAR(50) ucina 60-znakowy hash), przekazanie jako drugiego argumentu świeżego password_hash() czegoś zamiast zapisanego hasha, porównywanie z hashem zrobionym przez md5() albo dodatkowe białe znaki w zapisanej wartości. Sprawdź, czy strlen($hash) to 60, i przekazuj czyste hasło jako pierwszy argument.
Czy można odszyfrować wartość password_hash w PHP?
Nie. Hash jest jednokierunkowy: nie ma funkcji, która zamieni go z powrotem w hasło. Aby sprawdzić hasło, haszujesz próbę i porównujesz, i to właśnie robi password_verify(). Przy zapomnianym haśle wysyłaj link do resetu zamiast starego hasła.
Używać PASSWORD_DEFAULT, PASSWORD_BCRYPT czy PASSWORD_ARGON2ID?
PASSWORD_DEFAULT to zwykły wybór: dziś to bcrypt, a PHP może później przejść na silniejszy algorytm, który password_needs_rehash() pozwala wdrażać stopniowo. PASSWORD_ARGON2ID jest odporniejszy na łamanie przez GPU i można go używać, gdy twoja kompilacja PHP go zawiera.