Guarde senhas com password_hash($password, PASSWORD_DEFAULT) e verifique um login com password_verify($password, $hash), que retorna true ou false. Nunca guarde a senha em si, e nunca use md5() ou sha1() para isso.
Rode duas vezes: o hash é diferente a cada vez, e as duas versões são verificadas. O mesmo vale para todos os hashes desta página, então a sua saída não vai bater com a de mais ninguém.
Fazer login com password_verify
Um login compara a senha enviada com o hash salvo quando a conta foi criada. Este bloco "cadastra" um usuário e depois mostra um formulário de login; teste a senha certa (hunter2) e depois uma errada.
A mensagem de erro é a mesma para um e-mail desconhecido e uma senha errada. Dizer qual dos dois está errado conta a um atacante quais e-mails têm conta. Depois de um login bem-sucedido, um site real guarda o id do usuário na sessão, como mostra a página sobre sessões.
O que há dentro do hash
Um hash bcrypt é uma string que guarda tudo de que o password_verify() precisa: o algoritmo, o custo e o salt, seguidos do hash em si. O password_get_info() lê as primeiras partes de volta.
Como o salt fica guardado no hash, você precisa de uma única coluna no banco de dados, e dois usuários com a mesma senha ainda recebem hashes diferentes. Faça essa coluna VARCHAR(255): o bcrypt tem 60 caracteres, mas o PASSWORD_DEFAULT pode mudar para um algoritmo com saída mais longa.
Escolher o custo
O custo é o quanto cada hash é lento de propósito, e cada passo acima dobra o trabalho. O PHP 8.3 usa custo 10 para bcrypt (o PHP 8.4 aumentou o padrão para 12). Um login deve levar uns 100 a 300 ms de hashing; meça no seu próprio servidor:
Os números dependem da máquina, mas cada linha deve ser umas duas vezes a anterior. Mais alto é mais seguro contra quebra e mais lento para cada login, então escolha o custo mais alto que a sua página de login aguenta.
Atualizar hashes antigos com password_needs_rehash
Quando você aumenta o custo ou o PHP muda o PASSWORD_DEFAULT, os hashes existentes continuam funcionando, e o password_needs_rehash() diz quais estão desatualizados. O único momento em que você tem a senha em texto puro é no login, então é ali que você atualiza:
O prefixo $2y$10$ vira $2y$12$, e no próximo login o password_needs_rehash() retorna false.
Nunca use md5 ou sha1 para senhas
md5() e sha1() são hashes gerais rápidos e sem salt. Rápido é exatamente o errado para senhas: um atacante com uma tabela vazada consegue testar bilhões de palpites por segundo numa GPU, e um hash sem salt de uma senha comum já está em tabelas públicas de consulta. Compare a velocidade:
No tempo em que o password_hash() faz um hash, o md5 faz centenas de milhares, então cada palpite de quem tenta quebrar a senha fica esse tanto de vezes mais barato. Pesquise 5f4dcc3b5aa765d61d8327deb882cf99 e você encontra a palavra "password" na hora.
Se você herdar uma tabela de hashes md5, não dá para convertê-los diretamente. Envolva-os: guarde password_hash($oldMd5) e verifique com password_verify(md5($input), $stored), depois troque cada um por um hash normal quando aquele usuário fizer login de novo.
Argon2id e o limite de 72 bytes
O PASSWORD_ARGON2ID usa mais memória por hash, o que torna a quebra por GPU muito mais cara. Ele está disponível quando o PHP é compilado com a libargon2 (a maioria dos pacotes Linux é); verifique com defined('PASSWORD_ARGON2ID').
A última linha imprime bool(true): duas senhas diferentes batem porque o bcrypt ignora tudo depois do byte 72. Senhas reais raramente chegam a isso, mas frases-senha em japonês ou em outras escritas multibyte chegam a 72 bytes com uns 24 caracteres. Se você permite senhas muito longas, use Argon2id, que não tem esse limite. O password_verify() funciona com os dois algoritmos, então você pode trocar com o password_needs_rehash() como acima.
Perguntas frequentes
Por que o password_hash dá um resultado diferente a cada vez?
Cada chamada gera um novo salt aleatório e o guarda dentro do hash, então a mesma senha dá uma string diferente a cada vez. É intencional: o password_verify() lê o salt de volta do hash guardado, então continua retornando true para a senha certa.
Qual deve ser o tamanho da coluna de senha para o password_hash?
Um hash bcrypt do PASSWORD_DEFAULT tem 60 caracteres hoje, mas o algoritmo padrão pode mudar numa versão futura do PHP. Use VARCHAR(255) para que um hash mais longo caiba sem migração.
Por que o password_verify sempre retorna false?
As causas comuns são um hash truncado (uma coluna VARCHAR(50) corta o hash de 60 caracteres), passar um password_hash() novo de algo como segundo argumento em vez do hash que você guardou, comparar com um hash feito com md5(), ou espaços extras no valor guardado. Verifique se strlen($hash) é 60 e passe a senha em texto puro como primeiro argumento.
Posso descriptografar um valor do password_hash no PHP?
Não. Um hash é de mão única: não existe função que o transforme de volta na senha. Para verificar uma senha, você gera o hash da tentativa e compara, que é o que o password_verify() faz. Para uma senha esquecida, envie um link de redefinição em vez da senha antiga.
Devo usar PASSWORD_DEFAULT, PASSWORD_BCRYPT ou PASSWORD_ARGON2ID?
O PASSWORD_DEFAULT é a escolha comum: hoje é bcrypt e o PHP pode mudá-lo para um algoritmo mais forte depois, o que o password_needs_rehash() deixa você adotar aos poucos. O PASSWORD_ARGON2ID é mais forte contra quebra por GPU e pode ser usado quando o seu PHP o inclui.