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() e password_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

Exercícios

  1. 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';
    }
    ?>
  2. 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;
            }
        }
    }
  3. 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;
    ?>
  4. 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 processamento

    O 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.

  5. 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.