Speichere Passwörter mit password_hash($password, PASSWORD_DEFAULT) und prüfe einen Login mit password_verify($password, $hash), das true oder false liefert. Speichere nie das Passwort selbst, und verwende dafür nie md5() oder sha1().
Führe es zweimal aus: Der Hash ist jedes Mal anders, und beide Versionen bestehen die Prüfung. Dasselbe gilt für jeden Hash auf dieser Seite, also stimmt deine Ausgabe mit keiner anderen überein.
Mit password_verify anmelden
Ein Login prüft das gesendete Passwort gegen den Hash, der beim Anlegen des Kontos gespeichert wurde. Dieser Block „registriert“ einen Nutzer und zeigt dann ein Login-Formular; probier das richtige Passwort (hunter2) und dann ein falsches.
Die Fehlermeldung ist bei einer unbekannten E-Mail und einem falschen Passwort dieselbe. Zu sagen, was falsch war, verrät einem Angreifer, welche E-Mails ein Konto haben. Nach einem erfolgreichen Login speichert eine echte Website die Nutzer-ID in der Session, wie auf der Seite zu Sessions gezeigt.
Was im Hash steckt
Ein bcrypt-Hash ist ein String, der alles enthält, was password_verify() braucht: den Algorithmus, die Cost und das Salt, gefolgt vom eigentlichen Hash. password_get_info() liest die ersten Teile zurück.
Weil das Salt im Hash gespeichert ist, brauchst du nur eine Datenbankspalte, und zwei Nutzer mit demselben Passwort bekommen trotzdem verschiedene Hashes. Mach diese Spalte zu VARCHAR(255): bcrypt ist 60 Zeichen lang, aber PASSWORD_DEFAULT darf zu einem Algorithmus mit längerer Ausgabe wechseln.
Die Cost wählen
Die Cost bestimmt, wie langsam jeder Hash absichtlich ist, und jede Stufe nach oben verdoppelt die Arbeit. PHP 8.3 verwendet für bcrypt die Cost 10 (PHP 8.4 hat den Standard auf 12 angehoben). Ein Login sollte etwa 100 bis 300 ms mit Hashen verbringen; miss es auf deinem eigenen Server:
Die Zahlen hängen vom Rechner ab, aber jede Zeile sollte etwa doppelt so lang sein wie die davor. Höher ist sicherer gegen Knacken und langsamer bei jedem Login, also wähl die höchste Cost, die sich deine Login-Seite leisten kann.
Alte Hashes mit password_needs_rehash aktualisieren
Wenn du die Cost erhöhst oder PHP PASSWORD_DEFAULT ändert, funktionieren bestehende Hashes weiter, und password_needs_rehash() sagt dir, welche veraltet sind. Den einzigen Moment, in dem du das Klartext-Passwort hast, gibt es beim Login, also aktualisierst du dort:
Das Präfix $2y$10$ wird zu $2y$12$, und beim nächsten Login liefert password_needs_rehash() false.
Nie md5 oder sha1 für Passwörter
md5() und sha1() sind schnelle allgemeine Hashes ohne Salt. Schnell ist für Passwörter genau falsch: Ein Angreifer mit einer geleakten Tabelle kann auf einer GPU Milliarden Versuche pro Sekunde machen, und ein Hash ohne Salt eines gängigen Passworts steht bereits in öffentlichen Nachschlagetabellen. Vergleiche die Geschwindigkeit:
In der Zeit, in der password_hash() einen Hash erzeugt, erzeugt md5 Hunderttausende, also ist jeder Rateversuch eines Angreifers entsprechend billiger. Such nach 5f4dcc3b5aa765d61d8327deb882cf99, und du findest sofort das Wort „password“.
Wenn du eine Tabelle mit md5-Hashes erbst, kannst du sie nicht direkt umwandeln. Pack sie stattdessen ein: Speichere password_hash($oldMd5), prüfe mit password_verify(md5($input), $stored) und ersetze jeden davon durch einen normalen Hash, wenn sich dieser Nutzer das nächste Mal anmeldet.
Argon2id und die Grenze von 72 Bytes
PASSWORD_ARGON2ID verwendet pro Hash mehr Speicher, was das Knacken mit GPUs viel teurer macht. Es ist verfügbar, wenn PHP mit libargon2 gebaut wurde (bei den meisten Linux-Paketen der Fall); prüfe es mit defined('PASSWORD_ARGON2ID').
Die letzte Zeile gibt bool(true) aus: Zwei verschiedene Passwörter passen, weil bcrypt alles nach Byte 72 ignoriert. Echte Passwörter erreichen das selten, aber Passphrasen in Japanisch oder anderen Multibyte-Schriften erreichen 72 Bytes schon bei etwa 24 Zeichen. Wenn du sehr lange Passwörter erlaubst, verwende Argon2id, das keine solche Grenze hat. password_verify() funktioniert mit beiden Algorithmen, also kannst du wie oben mit password_needs_rehash() wechseln.
Häufig gestellte Fragen
Warum liefert password_hash jedes Mal ein anderes Ergebnis?
Jeder Aufruf erzeugt ein neues zufälliges Salt und speichert es im Hash, also ergibt dasselbe Passwort jedes Mal einen anderen String. Das ist gewollt: password_verify() liest das Salt aus dem gespeicherten Hash zurück und liefert deshalb für das richtige Passwort trotzdem true.
Wie lang sollte die Passwort-Spalte für password_hash sein?
Ein bcrypt-Hash aus PASSWORD_DEFAULT ist heute 60 Zeichen lang, aber der Standardalgorithmus kann sich in einer künftigen PHP-Version ändern. Verwende VARCHAR(255), damit ein längerer Hash ohne Migration passt.
Warum liefert password_verify immer false?
Die üblichen Ursachen sind ein abgeschnittener Hash (eine Spalte VARCHAR(50) kürzt den 60 Zeichen langen Hash), ein frisches password_hash() von irgendetwas als zweites Argument statt des gespeicherten Hashs, ein Vergleich mit einem Hash aus md5() oder zusätzlicher Leerraum im gespeicherten Wert. Prüfe, dass strlen($hash) 60 ist, und übergib das Klartext-Passwort als erstes Argument.
Kann ich in PHP einen Wert aus password_hash entschlüsseln?
Nein. Ein Hash ist eine Einbahnstraße: Es gibt keine Funktion, die ihn wieder in das Passwort verwandelt. Um ein Passwort zu prüfen, hashst du den Versuch und vergleichst, und genau das macht password_verify(). Bei einem vergessenen Passwort schickst du einen Link zum Zurücksetzen statt des alten Passworts.
Sollte ich PASSWORD_DEFAULT, PASSWORD_BCRYPT oder PASSWORD_ARGON2ID verwenden?
PASSWORD_DEFAULT ist die übliche Wahl: Heute ist es bcrypt, und PHP kann es später auf einen stärkeren Algorithmus umstellen, den du mit password_needs_rehash() schrittweise übernehmen kannst. PASSWORD_ARGON2ID ist stärker gegen Knacken mit GPUs und eine gute Wahl, wenn dein PHP-Build es enthält.