PHP

Sessions e Cookies Já leu

7 min de leitura

Sessions e Cookies
HTTP é um protocolo sem estado (stateless). Cada requisição é independente — o servidor não sabe, por padrão, que a requisição do usuário A às 14h05 foi feita pela mesma pessoa que fez a requisição às 14h03. Isso é ótimo

HTTP é um protocolo sem estado (stateless). Cada requisição é independente — o servidor não sabe, por padrão, que a requisição do usuário A às 14h05 foi feita pela mesma pessoa que fez a requisição às 14h03. Isso é ótimo para escalabilidade, mas péssimo para qualquer aplicação que precise de contexto: login, carrinho de compras, preferências de idioma.

Sessions e cookies são os mecanismos que adicionam estado ao HTTP. Sessions guardam dados no servidor; cookies guardam dados no navegador. Entender a diferença, os casos de uso de cada um, e como usá-los com segurança é fundamental.


Cookies — dados no navegador

Um cookie é um par chave-valor que o servidor envia ao navegador, e o navegador devolve automaticamente em todas as requisições seguintes para o mesmo domínio:

<?php

// Criar um cookie
// setcookie() deve ser chamado ANTES de qualquer saída HTML
setcookie(
    name:     'idioma',
    value:    'pt-BR',
    expires_or_options: time() + (86400 * 30), // 30 dias
    path:     '/',
    domain:   '',
    secure:   true,   // Apenas HTTPS
    httponly: true,   // JavaScript não pode acessar
);

// Ler um cookie
$idioma = $_COOKIE['idioma'] ?? 'pt-BR';

// Deletar um cookie: defina expires no passado
setcookie('idioma', '', time() - 3600, '/');

Os parâmetros secure: true e httponly: true são fundamentais para segurança. httponly impede que JavaScript leia o cookie — crucial para cookies de sessão, pois bloqueia ataques XSS que tentam roubar cookies.


Sessions — dados no servidor

Sessions funcionam assim: o servidor cria um identificador único (session ID), armazena dados no servidor associados a esse ID, e envia o ID ao navegador como cookie. Em cada requisição, o navegador devolve o cookie com o ID, e o servidor recupera os dados:

<?php

// Configurações de segurança ANTES de session_start()
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');   // Apenas HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', '1'); // Rejeita session IDs não iniciados pelo servidor

session_start();

// Armazenar dados na sessão
$_SESSION['usuario_id']   = 42;
$_SESSION['usuario_nome'] = 'Alice';
$_SESSION['logado_em']    = time();

// Ler dados da sessão
if (isset($_SESSION['usuario_id'])) {
    echo "Bem-vindo, " . htmlspecialchars($_SESSION['usuario_nome']) . "!";
}

// Remover um dado específico
unset($_SESSION['carrinho_temporario']);

// Destruir a sessão completamente (logout)
session_unset();
session_destroy();

// Também limpe o cookie
setcookie(session_name(), '', time() - 3600, '/');

Regeneração de session ID — prevenindo session fixation

Um ataque de session fixation acontece quando um atacante força a vítima a usar um session ID conhecido. Ao fazer login, sempre regenere o ID:

<?php
session_start();

function fazerLogin(string $email, string $senha, PDO $pdo): bool
{
    $stmt = $pdo->prepare('SELECT id, nome, senha_hash FROM usuarios WHERE email = ?');
    $stmt->execute([$email]);
    $usuario = $stmt->fetch();

    if (!$usuario || !password_verify($senha, $usuario['senha_hash'])) {
        return false;
    }

    // CRÍTICO: regenerar o ID antes de armazenar dados de autenticação
    // Invalida o session ID antigo, cria um novo
    session_regenerate_id(delete_old_session: true);

    $_SESSION['usuario_id']   = $usuario['id'];
    $_SESSION['usuario_nome'] = $usuario['nome'];
    $_SESSION['logado_em']    = time();

    return true;
}

Sistema de autenticação completo

<?php
declare(strict_types=1);

class Auth
{
    private const SESSION_LIFETIME = 7200; // 2 horas
    private const MAX_TENTATIVAS   = 5;

    public function __construct(private readonly PDO $pdo) {}

    public function login(string $email, string $senha): bool
    {
        // Proteção contra brute-force
        if ($this->excedeuentativas($email)) {
            throw new RuntimeException('Muitas tentativas. Tente novamente em 15 minutos.');
        }

        $stmt = $this->pdo->prepare(
            'SELECT id, nome, senha_hash FROM usuarios WHERE email = ? AND ativo = 1'
        );
        $stmt->execute([$email]);
        $usuario = $stmt->fetch();

        if (!$usuario || !password_verify($senha, $usuario['senha_hash'])) {
            $this->registrarTentativa($email);
            return false;
        }

        $this->limparTentativas($email);
        session_regenerate_id(delete_old_session: true);

        $_SESSION['uid']       = $usuario['id'];
        $_SESSION['nome']      = $usuario['nome'];
        $_SESSION['inicio']    = time();
        $_SESSION['ip']        = $_SERVER['REMOTE_ADDR'];
        $_SESSION['ua']        = $_SERVER['HTTP_USER_AGENT'] ?? '';

        return true;
    }

    public function verificar(): bool
    {
        if (!isset($_SESSION['uid'])) return false;

        // Timeout por inatividade
        if (time() - $_SESSION['inicio'] > self::SESSION_LIFETIME) {
            $this->logout();
            return false;
        }

        // Validação básica de IP e user agent (proteção contra roubo de sessão)
        if ($_SESSION['ip'] !== ($_SERVER['REMOTE_ADDR'] ?? '')) {
            $this->logout();
            return false;
        }

        // Atualiza o timestamp de última atividade
        $_SESSION['inicio'] = time();
        return true;
    }

    public function logout(): void
    {
        session_unset();
        session_destroy();
        setcookie(session_name(), '', time() - 3600, '/', secure: true, httponly: true);
    }

    public function usuarioId(): ?int
    {
        return $_SESSION['uid'] ?? null;
    }

    private function excedeuentativas(string $email): bool
    {
        $stmt = $this->pdo->prepare(
            "SELECT COUNT(*) FROM tentativas_login
             WHERE email = ? AND criado_em > datetime('now', '-15 minutes')"
        );
        $stmt->execute([$email]);
        return (int) $stmt->fetchColumn() >= self::MAX_TENTATIVAS;
    }

    private function registrarTentativa(string $email): void
    {
        $this->pdo->prepare('INSERT INTO tentativas_login (email, ip) VALUES (?, ?)')
            ->execute([$email, $_SERVER['REMOTE_ADDR'] ?? '']);
    }

    private function limparTentativas(string $email): void
    {
        $this->pdo->prepare('DELETE FROM tentativas_login WHERE email = ?')
            ->execute([$email]);
    }
}

Flash messages

Flash messages são mensagens de sessão que existem apenas até a próxima requisição — exibem feedback de operações após um redirect:

<?php
class Flash
{
    public static function adicionar(string $tipo, string $mensagem): void
    {
        $_SESSION['flash'][] = ['tipo' => $tipo, 'mensagem' => $mensagem];
    }

    public static function obterETodos(): array
    {
        $mensagens = $_SESSION['flash'] ?? [];
        unset($_SESSION['flash']);
        return $mensagens;
    }
}

// Uso: após salvar com sucesso
Flash::adicionar('sucesso', 'Produto cadastrado com sucesso!');
header('Location: /produtos');
exit;

// Na página de destino (produtos.php):
foreach (Flash::obterETodos() as $flash) {
    echo "<div class='alerta {$flash['tipo']}'>{$flash['mensagem']}</div>";
}

Cookies de longo prazo — "Lembrar de mim"

O "Lembrar de mim" não deve estender a sessão PHP (que fica na memória do servidor). Deve usar um cookie separado com um token seguro persistido no banco:

<?php

function criarTokenLembrarMe(int $usuarioId, PDO $pdo): void
{
    $token = bin2hex(random_bytes(32)); // 64 chars hex, criptograficamente seguro
    $hash  = hash('sha256', $token);   // Armazena o hash, não o token

    $pdo->prepare(
        'INSERT INTO tokens_lembrar_me (usuario_id, token_hash, expira_em)
         VALUES (?, ?, datetime("now", "+30 days"))'
    )->execute([$usuarioId, $hash]);

    setcookie('lembrar_me', $token, [
        'expires'  => time() + 86400 * 30,
        'path'     => '/',
        'secure'   => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
}

function verificarTokenLembrarMe(PDO $pdo): ?int
{
    $token = $_COOKIE['lembrar_me'] ?? null;
    if (!$token) return null;

    $hash = hash('sha256', $token);

    $stmt = $pdo->prepare(
        "SELECT usuario_id FROM tokens_lembrar_me
         WHERE token_hash = ? AND expira_em > datetime('now')"
    );
    $stmt->execute([$hash]);
    $row = $stmt->fetch();

    if (!$row) return null;

    // Rotação de token: invalida o usado e cria um novo
    $pdo->prepare('DELETE FROM tokens_lembrar_me WHERE token_hash = ?')->execute([$hash]);
    criarTokenLembrarMe($row['usuario_id'], $pdo);

    return (int) $row['usuario_id'];
}

Resumo

Mecanismo Dados ficam onde Validade Caso de uso
Cookie Navegador Configurável Preferências, lembrar-me
Session PHP Servidor Até fechar o browser ou timeout Autenticação, carrinho
Token lembrar-me Banco + cookie Longa (30 dias) Login persistente seguro

Exercício da semana

  1. Implemente um sistema de login completo: formulário, verificação de senha com password_hash/password_verify, regeneração de session ID, middleware de proteção para páginas autenticadas.

  2. Adicione flash messages ao sistema: após login com sucesso, redirecione com mensagem "Bem-vindo, {nome}!". Após logout, redirecione com "Você saiu com sucesso."

  3. Implemente "Lembrar de mim" com rotação de token. Teste que o token antigo é invalidado após cada uso.

  4. Crie um painel /admin/sessoes que liste as sessões ativas do banco (requer salvar metadados de sessão em tabela própria). Implemente um botão "Encerrar esta sessão" que invalida o token.

  5. Desafio: implemente um handler de sessão customizado (SessionHandlerInterface) que armazena sessões no banco de dados em vez de arquivos — necessário para aplicações rodando em múltiplos servidores.


Referências

  • PHP Manual — Sessions: https://www.php.net/manual/pt_BR/book.session.php
  • PHP Manual — setcookie: https://www.php.net/manual/pt_BR/function.setcookie.php
  • OWASP — Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
  • Artigo sobre tokens "Lembrar de mim" seguro: https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
Comentários

Mais em PHP

Funções
Funções

At&eacute; agora escrevemos c&oacute;digo sequencial &mdash; uma instru&ccedi...

Artigo 50 — Projeto Final Parte 1: Planejamento e Arquitetura
Artigo 50 — Projeto Final Parte 1: Planejamento e Arquitetura

Chegamos ao projeto final. Quarenta e nove artigos de fundamentos, orientação...

Artigo 41 — Introdução ao Laravel
Artigo 41 — Introdução ao Laravel

Laravel é o framework PHP mais popular do mundo, e por boas razões. Ele não a...