Autenticação
Esta aula aborda a implementação de autenticação em PHP, desde o login com sessões até o logout seguro, incluindo a funcionalidade 'lembrar-me' e boas práticas de segurança. O conteúdo é prático, com exemplos de código e exercícios para fixação.
A autenticação é um dos pilares de qualquer aplicação web que necessite identificar usuários. Nesta aula, vamos explorar como implementar um sistema de autenticação completo em PHP, cobrindo desde o login tradicional com sessões até o logout seguro e a funcionalidade "lembrar-me". Ao final, você terá uma base sólida para construir sistemas de login robustos e seguros.
Dominar a autenticação é essencial para proteger dados sensíveis e garantir que apenas usuários autorizados acessem determinadas áreas de uma aplicação. Vamos abordar conceitos fundamentais de sessões, cookies, hashing de senhas e boas práticas de segurança, tudo com exemplos práticos e código funcional.
Login e sessão
O login é o processo de verificar as credenciais de um usuário (geralmente e-mail ou usuário e senha) e, se válidas, iniciar uma sessão para manter o estado de autenticação. Em PHP, isso é feito com a superglobal $_SESSION, que persiste dados entre requisições.
Para começar, precisamos de um formulário de login e um script que processe os dados. O fluxo básico é: receber os dados, validar, verificar no banco de dados (ou outra fonte), e se estiver correto, armazenar informações na sessão.
Aqui está um exemplo de formulário de login em HTML:
<form method="post" action="login.php">
<label>E-mail:</label>
<input type="email" name="email" required>
<br>
<label>Senha:</label>
<input type="password" name="password" required>
<br>
<button type="submit">Entrar</button>
</form>No script de login, precisamos iniciar a sessão e verificar as credenciais. Vamos usar password_verify para comparar a senha fornecida com o hash armazenado.
<?php
session_start();
require_once 'db.php'; // conexão com o banco
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'];
$password = $_POST['password'];
// Buscar usuário no banco
$stmt = $pdo->prepare('SELECT id, email, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// Credenciais corretas: criar sessão
session_regenerate_id(true); // evita fixação de sessão
$_SESSION['user_id'] = $user['id'];
$_SESSION['email'] = $user['email'];
header('Location: dashboard.php');
exit;
} else {
$error = "Credenciais inválidas.";
}
}
?>É crucial usar session_regenerate_id() após o login para prevenir ataques de fixação de sessão. Isso cria um novo ID de sessão, descartando o antigo.
Para proteger páginas restritas, basta verificar se a variável de sessão existe:
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Resto do código da página protegida
?>Lembrar-me
O recurso "lembrar-me" permite que o usuário permaneça autenticado mesmo após fechar o navegador, usando cookies persistentes. É importante implementar isso com segurança, evitando armazenar credenciais em cookies.
A abordagem comum é gerar um token aleatório, armazená-lo no banco de dados associado ao usuário e enviar um cookie com esse token. Quando o usuário retorna, o token é verificado e a sessão é restaurada.
Vamos implementar passo a passo. Primeiro, no login, se o usuário marcar "lembrar-me", geramos um token e salvamos no banco:
<?php
// dentro do login, após verificação
if (isset($_POST['remember'])) {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 30 * 24 * 3600); // 30 dias
$stmt = $pdo->prepare('UPDATE users SET remember_token = ?, remember_expires = ? WHERE id = ?');
$stmt->execute([$token, $expires, $user['id']]);
setcookie('remember_me', $token, time() + 30 * 24 * 3600, '/', '', true, true); // HttpOnly e Secure
}
?>Quando um usuário visita o site sem sessão ativa, verificamos o cookie:
<?php
if (!isset($_SESSION['user_id']) && isset($_COOKIE['remember_me'])) {
$token = $_COOKIE['remember_me'];
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE remember_token = ? AND remember_expires > NOW()');
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['email'] = $user['email'];
}
}
?>É fundamental usar cookies com as flags HttpOnly e Secure (em HTTPS). O token deve ser armazenado em hash no banco, não em texto puro. Para maior segurança, considere usar tokens de dois fatores ou rotação de tokens.
Logout seguro
O logout deve encerrar a sessão corretamente e, se aplicável, invalidar o token de "lembrar-me". Um logout mal implementado pode deixar resquícios de autenticação.
Para um logout seguro, devemos:
- Destruir a sessão
- Limpar o cookie de "lembrar-me"
- Invalidar o token no banco
Aqui está um script de logout robusto:
<?php
session_start();
// Se houver cookie de lembrar-me, invalidar no banco
if (isset($_COOKIE['remember_me'])) {
$token = $_COOKIE['remember_me'];
$stmt = $pdo->prepare('UPDATE users SET remember_token = NULL WHERE remember_token = ?');
$stmt->execute([$token]);
// Expirar o cookie
setcookie('remember_me', '', time() - 3600, '/', '', true, true);
}
// Destruir a sessão
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
}
session_destroy();
header('Location: login.php');
exit;
?>O uso de session_destroy() remove os dados da sessão, e a limpeza do cookie de sessão é feita manualmente. Para o cookie "lembrar-me", usamos um valor vazio e data de expiração no passado.
Boas práticas
Além das técnicas apresentadas, é essencial adotar boas práticas de segurança para proteger a autenticação. Aqui estão algumas recomendações:
- Use HTTPS em todo o site para evitar interceptação de credenciais.
- Utilize
password_hash()epassword_verify()para senhas, nunca armazene em texto puro. - Implemente proteção contra CSRF em formulários de login e logout.
- Limite tentativas de login para evitar força bruta.
- Use
session_regenerate_id()em eventos importantes, como login e mudança de privilégios. - Defina um tempo de expiração para sessões inativas.
- Armazene tokens de "lembrar-me" em hash (por exemplo, SHA-256) para mitigar vazamentos.
- Nunca inclua informações sensíveis na URL.
Um exemplo de proteção CSRF no formulário de login:
<?php
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
?>
<form method="post" action="login.php">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<!-- campos do formulário -->
</form>No processamento, verificamos o token. Isso impede que sites maliciosos enviem requisições forjadas.
Referências
- PHP Manual - Sessões
- PHP Manual - password_hash
- PHP Manual - password_verify
- OWASP - Session Fixation
- OWASP - CSRF
- OWASP - Session Management Cheat Sheet
- MDN - Cookies HTTP
- PHP Manual - Cookies
Exercícios
- Implemente um sistema de login simples que use sessões e verifique as credenciais em um array fixo de usuários (não banco). Inclua proteção contra fixação de sessão.✓ Resposta:
<?php session_start(); $users = [ ['id' => 1, 'email' => 'user@example.com', 'password' => password_hash('123456', PASSWORD_DEFAULT)] ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $email = $_POST['email']; $password = $_POST['password']; foreach ($users as $user) { if ($user['email'] === $email && password_verify($password, $user['password'])) { session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; header('Location: dashboard.php'); exit; } } $error = 'Credenciais inválidas'; } ?> - Adicione a funcionalidade "lembrar-me" ao sistema anterior, armazenando o token em um arquivo JSON (simulando banco). O cookie deve ser seguro e HttpOnly.✓ Resposta:
// Supondo que os usuários estejam em um arquivo users.json // Estrutura: [{"id":1,"email":"...","password":"hash","remember_token":null,"remember_expires":null}] // No login, se marcar lembrar: if (isset($_POST['remember'])) { $token = bin2hex(random_bytes(32)); $expires = date('Y-m-d H:i:s', time() + 30*24*3600); // Atualizar no arquivo $users = json_decode(file_get_contents('users.json'), true); foreach ($users as &$u) { if ($u['id'] == $user['id']) { $u['remember_token'] = hash('sha256', $token); $u['remember_expires'] = $expires; break; } } file_put_contents('users.json', json_encode($users)); setcookie('remember_me', $token, time()+30*24*3600, '/', '', true, true); } // Na checagem do cookie: if (!isset($_SESSION['user_id']) && isset($_COOKIE['remember_me'])) { $token = $_COOKIE['remember_me']; $tokenHash = hash('sha256', $token); $users = json_decode(file_get_contents('users.json'), true); foreach ($users as $u) { if (hash_equals($u['remember_token'], $tokenHash) && strtotime($u['remember_expires']) > time()) { session_regenerate_id(true); $_SESSION['user_id'] = $u['id']; break; } } } - Crie um script de logout seguro que invalide o token "lembrar-me" e destrua a sessão completamente.✓ Resposta:
<?php session_start(); if (isset($_COOKIE['remember_me'])) { $token = $_COOKIE['remember_me']; $tokenHash = hash('sha256', $token); // Remover token do arquivo $users = json_decode(file_get_contents('users.json'), true); foreach ($users as &$u) { if (hash_equals($u['remember_token'], $tokenHash)) { $u['remember_token'] = null; $u['remember_expires'] = null; break; } } file_put_contents('users.json', json_encode($users)); setcookie('remember_me', '', time()-3600, '/', '', true, true); } $_SESSION = []; if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time()-42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); } session_destroy(); header('Location: login.php'); exit; ?> - Implemente uma proteção CSRF no formulário de login e verifique no processamento. Explique como isso funciona.✓ Resposta:
// No formulário: <?php session_start(); $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); ?> <form method="post" action="login.php"> <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>"> <!-- ... --> </form> // No processamento: if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { die('Token CSRF inválido'); } // Restante do processamentoO token CSRF é um valor aleatório gerado na sessão e colocado no formulário. Quando o formulário é submetido, comparamos o token enviado com o da sessão. Como o atacante não tem acesso à sessão do usuário, ele não pode forjar um formulário válido.
- Escreva um código que limite as tentativas de login (por exemplo, bloquear após 5 tentativas em 10 minutos) usando sessão ou banco de dados.✓ Resposta:
// Usando sessão para armazenar tentativas session_start(); if (!isset($_SESSION['login_attempts'])) { $_SESSION['login_attempts'] = []; } // Limpar tentativas antigas (mais de 10 min) $_SESSION['login_attempts'] = array_filter($_SESSION['login_attempts'], function($t) { return $t > time() - 600; }); if (count($_SESSION['login_attempts']) >= 5) { die('Muitas tentativas. Tente novamente mais tarde.'); } // No processamento, se falhar: $_SESSION['login_attempts'][] = time(); // Se sucesso, limpar tentativas: unset($_SESSION['login_attempts']);
Observações finais
A autenticação é uma área crítica e em constante evolução. Sempre mantenha suas bibliotecas e dependências atualizadas, e acompanhe as recomendações de segurança como as do OWASP. Teste seu sistema de login exaustivamente, incluindo cenários de erro e ataques comuns.
Com as práticas apresentadas, você estará no caminho certo para construir aplicações PHP seguras e confiáveis.