Guarda las contraseñas con password_hash($password, PASSWORD_DEFAULT) y comprueba un inicio de sesión con password_verify($password, $hash), que devuelve true o false. Nunca guardes la propia contraseña, y nunca uses md5() ni sha1() para ello.
Ejecútalo dos veces: el hash es distinto cada vez, y las dos versiones se verifican. Lo mismo vale para todos los hashes de esta página, así que tu salida no coincidirá con la de nadie más.
Iniciar sesión con password_verify
Un inicio de sesión comprueba la contraseña enviada contra el hash guardado cuando se creó la cuenta. Este bloque "registra" a un usuario y luego muestra un formulario de inicio de sesión; prueba con la contraseña correcta (hunter2) y luego con una incorrecta.
El mensaje de error es el mismo para un email desconocido y para una contraseña incorrecta. Decir cuál de los dos falló le indica a un atacante qué emails tienen cuenta. Tras un inicio de sesión correcto, un sitio real guarda el id del usuario en la sesión, como se muestra en la página de sesiones.
Qué hay dentro del hash
Un hash bcrypt es una sola cadena que contiene todo lo que necesita password_verify(): el algoritmo, el coste y la sal, seguidos del propio hash. password_get_info() lee de vuelta las primeras partes.
Como la sal está guardada en el hash, solo necesitas una columna en la base de datos, y dos usuarios con la misma contraseña siguen teniendo hashes distintos. Haz esa columna VARCHAR(255): bcrypt ocupa 60 caracteres, pero PASSWORD_DEFAULT puede cambiar a un algoritmo con una salida más larga.
Elegir el coste
El coste es lo lento que es cada hash a propósito, y cada paso hacia arriba duplica el trabajo. PHP 8.3 usa coste 10 para bcrypt (PHP 8.4 subió el valor por defecto a 12). Un inicio de sesión debería tardar aproximadamente de 100 a 300 ms en el hash; mídelo en tu propio servidor:
Los números dependen de la máquina, pero cada línea debería ser más o menos el doble de la anterior. Más alto es más seguro frente al crackeo y más lento en cada inicio de sesión, así que elige el coste más alto que se pueda permitir tu página de inicio de sesión.
Actualizar hashes antiguos con password_needs_rehash
Cuando subes el coste o PHP cambia PASSWORD_DEFAULT, los hashes existentes siguen funcionando, y password_needs_rehash() te dice cuáles están desactualizados. El único momento en que tienes la contraseña en texto plano es el inicio de sesión, así que es ahí donde actualizas:
El prefijo $2y$10$ pasa a ser $2y$12$, y en el siguiente inicio de sesión password_needs_rehash() devuelve false.
Nunca uses md5 ni sha1 para contraseñas
md5() y sha1() son hashes generales rápidos y sin sal. Rápido es justo lo contrario de lo que necesitan las contraseñas: un atacante con una tabla filtrada puede probar miles de millones de intentos por segundo en una GPU, y el hash sin sal de una contraseña común ya está en tablas de búsqueda públicas. Compara la velocidad:
En el tiempo en que password_hash() hace un hash, md5 hace cientos de miles, así que cada intento de quien crackea es esa misma cantidad de veces más barato. Busca 5f4dcc3b5aa765d61d8327deb882cf99 y encontrarás al instante la palabra "password".
Si heredas una tabla de hashes md5, no puedes convertirlos directamente. Envuélvelos: guarda password_hash($oldMd5) y verifica con password_verify(md5($input), $stored), y luego sustituye cada uno por un hash normal la próxima vez que ese usuario inicie sesión.
Argon2id y el límite de 72 bytes
PASSWORD_ARGON2ID usa más memoria por hash, lo que encarece mucho el crackeo con GPU. Está disponible cuando PHP se compila con libargon2 (la mayoría de los paquetes de Linux lo hacen); compruébalo con defined('PASSWORD_ARGON2ID').
La última línea imprime bool(true): dos contraseñas distintas coinciden porque bcrypt ignora todo lo que hay después del byte 72. Las contraseñas reales rara vez llegan a eso, pero las frases de contraseña en japonés u otras escrituras multibyte alcanzan los 72 bytes con unos 24 caracteres. Si permites contraseñas muy largas, usa Argon2id, que no tiene ese límite. password_verify() funciona con los dos algoritmos, así que puedes cambiar con password_needs_rehash() como arriba.
Preguntas frecuentes
¿Por qué password_hash da un resultado distinto cada vez?
Cada llamada genera una sal aleatoria nueva y la guarda dentro del hash, así que la misma contraseña da una cadena distinta cada vez. Es intencionado: password_verify() vuelve a leer la sal del hash guardado, así que sigue devolviendo true con la contraseña correcta.
¿Qué longitud debe tener la columna de la contraseña para password_hash?
Un hash bcrypt de PASSWORD_DEFAULT tiene hoy 60 caracteres, pero el algoritmo por defecto puede cambiar en una versión futura de PHP. Usa VARCHAR(255) para que quepa un hash más largo sin migración.
¿Por qué password_verify siempre devuelve false?
Las causas habituales son un hash truncado (una columna VARCHAR(50) corta el hash de 60 caracteres), pasar como segundo argumento un password_hash() recién hecho de algo en lugar del hash que guardaste, comparar con un hash hecho con md5() o espacios de más en el valor guardado. Comprueba que strlen($hash) sea 60 y pasa la contraseña en texto plano como primer argumento.
¿Puedo descifrar un valor de password_hash en PHP?
No. Un hash es de un solo sentido: no hay ninguna función que lo convierta de nuevo en la contraseña. Para comprobar una contraseña haces el hash del intento y comparas, que es lo que hace password_verify(). Para una contraseña olvidada, envía un enlace de restablecimiento en lugar de la contraseña antigua.
¿Uso PASSWORD_DEFAULT, PASSWORD_BCRYPT o PASSWORD_ARGON2ID?
PASSWORD_DEFAULT es la opción habitual: hoy es bcrypt y PHP puede cambiarlo más adelante a un algoritmo más fuerte, que password_needs_rehash() te permite adoptar poco a poco. PASSWORD_ARGON2ID es más resistente al crackeo con GPU y está bien usarlo cuando tu PHP lo incluye.