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
-
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. -
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."
-
Implemente "Lembrar de mim" com rotação de token. Teste que o token antigo é invalidado após cada uso.
-
Crie um painel
/admin/sessoesque 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. -
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