fingerprint 5. Hash de Senhas com bcrypt
Armazenar senhas em texto plano é o erro mais grave que um desenvolvedor pode cometer. Se seu banco for invadido, todas as senhas ficarão expostas. E pior: como usuários reutilizam senhas, você estaria comprometendo contas em outros sistemas (email, bancos, redes sociais).
O bcrypt é um algoritmo de hash projetado especificamente para senhas. Ele possui três características essenciais: é unidirecional (impossível reverter), adiciona salt automático (cada hash é único mesmo para senhas iguais) e é computacionalmente caro (dificulta ataques de força bruta).
O PHP oferece as funções password_hash() e password_verify()
que implementam bcrypt com configuração segura por padrão. Você nunca precisa gerenciar salt manualmente
ou escolher algoritmos - o PHP faz tudo automaticamente, seguindo as melhores práticas atuais.
password_hash() sem
especificar PASSWORD_BCRYPT ou PASSWORD_DEFAULT.
compare Hash vs Criptografia: Qual a Diferença?
lock Criptografia (Reversível)
- Propósito: Proteger dados em trânsito
- Direção: Bidirecional (encrypt + decrypt)
- Uso: HTTPS, arquivos confidenciais
- Exemplo: AES, RSA
- Velocidade: Muito rápida
tag Hash (Irreversível)
- Propósito: Verificar autenticidade
- Direção: Unidirecional (não pode reverter)
- Uso: Senhas, checksums
- Exemplo: bcrypt, Argon2
- Velocidade: Intencionalmente lenta
code Implementando bcrypt no PHP
1️⃣ Criando Hash ao Registrar Usuário
false, 'message' => 'Senha deve ter no mínimo 8 caracteres']);
exit;
}
// Criar hash (AUTOMÁTICO: salt + custo otimizado)
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// O hash gerado tem este formato:
// $2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// │ │ │ │
// │ │ └── Salt (22 caracteres) │
// │ └── Custo (10 = 2^10 iterações = 1024x) │
// └── Algoritmo (2y = bcrypt) │
// Hash final (31 chars) ──┘
try {
// Inserir no banco
$stmt = $pdo->prepare('
INSERT INTO users (email, password_hash, created_at)
VALUES (?, ?, NOW())
');
$stmt->execute([$email, $passwordHash]);
$userId = $pdo->lastInsertId();
echo json_encode([
'success' => true,
'message' => 'Usuário criado com sucesso',
'user_id' => $userId
]);
} catch (PDOException $e) {
if ($e->getCode() == 23000) { // Duplicate entry
http_response_code(409);
echo json_encode(['success' => false, 'message' => 'Email já cadastrado']);
} else {
error_log("Register Error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Erro ao criar usuário']);
}
}
?>
2️⃣ Verificando Senha no Login
prepare('SELECT id, email, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
// Verificar senha
if (!$user || !password_verify($password, $user['password_hash'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Credenciais inválidas']);
exit;
}
// password_verify() faz DUAS coisas:
// 1. Extrai o salt e algoritmo do hash armazenado
// 2. Hasheia a senha fornecida com o mesmo salt
// 3. Compara os dois hashes em tempo constante (previne timing attacks)
// Se chegou aqui, senha está correta
$token = generateJWT($user['id']);
echo json_encode([
'success' => true,
'token' => $token,
'user' => ['id' => $user['id'], 'email' => $user['email']]
]);
?>
password_verify() usa
comparação em tempo constante, prevenindo timing attacks onde atacantes medem
o tempo de resposta para deduzir caracteres corretos.
tune Ajustando o Custo Computacional
O parâmetro cost define quantas vezes o algoritmo será executado.
Quanto maior, mais seguro, mas mais lento. O padrão é 10 (2^10 = 1024 iterações).
$cost]);
$end = microtime(true);
$elapsed = $end - $start;
$cost++;
} while ($elapsed < $targetTime && $cost < 14);
return $cost - 1;
}
// Usar custo customizado
$optimalCost = findOptimalCost(); // Exemplo: 12
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => $optimalCost]);
// Resultado com cost=12:
// $2y$12$... (mais seguro, ~400ms por hash)
?>
Cost = 8 (Fraco)
~50ms por hash
256 iterações
Vulnerável a força bruta
Cost = 10 (Padrão)
~100-200ms por hash
1024 iterações
Recomendado
Cost = 12 (Forte)
~400-800ms por hash
4096 iterações
Alta segurança
update Rehashing: Atualizando Algoritmo
Com o tempo, algoritmos se tornam obsoletos. O PHP permite verificar se um hash precisa ser atualizado e fazer isso transparentemente no login do usuário.
fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Credenciais inválidas']);
exit;
}
// Verificar se precisa atualizar o hash
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
// Gerar novo hash com algoritmo/custo atualizado
$newHash = password_hash($password, PASSWORD_DEFAULT);
// Atualizar no banco
$stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$stmt->execute([$newHash, $user['id']]);
error_log("Password rehashed for user ID: " . $user['id']);
}
// Continuar com login normal...
?>
rule Validação de Força de Senha
bool, 'errors' => array]
*/
function validatePasswordStrength($password) {
$errors = [];
// Comprimento mínimo
if (strlen($password) < 8) {
$errors[] = 'Senha deve ter no mínimo 8 caracteres';
}
// Letra maiúscula
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'Senha deve conter ao menos uma letra maiúscula';
}
// Letra minúscula
if (!preg_match('/[a-z]/', $password)) {
$errors[] = 'Senha deve conter ao menos uma letra minúscula';
}
// Número
if (!preg_match('/[0-9]/', $password)) {
$errors[] = 'Senha deve conter ao menos um número';
}
// Caractere especial
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = 'Senha deve conter ao menos um caractere especial';
}
// Senhas comuns (lista reduzida, use biblioteca completa em produção)
$commonPasswords = ['password', '12345678', 'qwerty', 'abc123', 'password123'];
if (in_array(strtolower($password), $commonPasswords)) {
$errors[] = 'Senha muito comum, escolha outra';
}
return [
'valid' => empty($errors),
'errors' => $errors
];
}
// Uso
$validation = validatePasswordStrength($data['password']);
if (!$validation['valid']) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Senha inválida',
'errors' => $validation['errors']
]);
exit;
}
?>