HTTP Cookies Segurança
Redes & HTTP

Request & Response

HTTP é um protocolo de texto — um pedido é uma mensagem estruturada que o browser envia ao servidor, e uma resposta é a mensagem estruturada que o servidor devolve. Perceber a anatomia exacta de cada mensagem — métodos, headers, status codes, e o mecanismo de cookies — é perceber onde o JWT viaja, como o browser o envia automaticamente em cada pedido, e porque o HttpOnly é a defesa correcta contra XSS.

Anatomia de um Pedido HTTP

Um pedido HTTP tem três partes: a linha de início com o método e o caminho, os headers com metadados, e o body com dados opcionais. Uma linha vazia separa os headers do body.

// Pedido HTTP completo — formato em texto (dentro do canal TLS)

POST /api/auth/login HTTP/1.1          ← linha de início: método + path + versão
Host: app.randomt.pt                   ← header obrigatório: identifica o servidor virtual
Content-Type: application/json         ← formato do body
Content-Length: 52                     ← tamanho do body em bytes
Accept: application/json               ← formatos aceites na resposta
Origin: https://app.randomt.pt         ← origem do pedido (usado pelo CORS)
                                       ← linha vazia obrigatória — separa headers do body
{"email":"ana@example.com","password":"secreta123"}   ← body (JSON)


// Pedido GET — sem body (GETs não têm body por convenção)

GET /api/accounts/42 HTTP/1.1
Host: app.randomt.pt
Accept: application/json
Cookie: access_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbmFAZXhhbXBsZS5jb20ifQ.abc123
        ↑
        o JWT viaja aqui — enviado automaticamente pelo browser em cada pedido
        o JavaScript não tem acesso a este valor — está em HttpOnly cookie

Métodos HTTP

O método define a intenção do pedido — o que o cliente quer fazer com o recurso identificado pelo URL. REST usa os métodos como verbos semânticos sobre substantivos (os recursos).

MétodoSemânticaBodyIdempotenteExemplo
GET Ler um recurso GET /api/accounts/42
POST Criar um recurso novo POST /api/accounts
PUT Substituir um recurso completo PUT /api/accounts/42
PATCH Modificar parcialmente um recurso PATCH /api/accounts/42
DELETE Eliminar um recurso DELETE /api/accounts/42
OPTIONS Pré-voo CORS — "posso fazer este pedido?" Enviado automaticamente pelo browser antes de pedidos cross-origin
Idempotência: porquê importa Um método é idempotente quando executá-lo múltiplas vezes produz o mesmo resultado que executá-lo uma única vez. DELETE /accounts/42 executado dez vezes tem o mesmo efeito que executado uma vez — a conta está eliminada. POST /accounts executado dez vezes cria dez contas. Esta propriedade é crítica para retry automático em falhas de rede: só é seguro repetir pedidos idempotentes.

Headers: os Metadados do Protocolo

Os headers transportam metadados sobre o pedido ou a resposta — formato do conteúdo, autenticação, cache, origem, cookies. São pares Nome: Valor, um por linha, case-insensitive no nome. Alguns são definidos pelo protocolo HTTP; outros são convenções da aplicação.

// Headers de pedido mais relevantes para desenvolvimento Spring Boot:

// Identificação do conteúdo
Content-Type: application/json          // formato do body enviado
Accept: application/json               // formatos aceites na resposta
Content-Length: 248                    // tamanho do body em bytes

// Autenticação (as duas abordagens — apenas uma é correcta)
Authorization: Bearer eyJhbGci...      // ❌ JWT no header — exposto ao JavaScript
Cookie: access_token=eyJhbGci...       // ✅ JWT em HttpOnly cookie — JS não acede

// Contexto do browser
Origin: https://app.randomt.pt         // origem do pedido — usado pelo CORS
Referer: https://app.randomt.pt/login  // página de onde veio o pedido
User-Agent: Mozilla/5.0 ...            // identificação do browser

// Cache
Cache-Control: no-cache                // não usar cache — sempre pedir ao servidor
If-None-Match: "abc123"               // condicional: só responder se o recurso mudou

// Headers de resposta mais relevantes:

// Tipo de conteúdo
Content-Type: application/json; charset=utf-8

// Cookies — o servidor instrui o browser a guardar um cookie
Set-Cookie: access_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900

// Segurança
Strict-Transport-Security: max-age=31536000; includeSubDomains  // HSTS
X-Content-Type-Options: nosniff        // impede browser de "adivinhar" o Content-Type
X-Frame-Options: DENY                  // impede embedding em iframe (clickjacking)
Content-Security-Policy: default-src 'self'  // restringe origens de recursos

// CORS
Access-Control-Allow-Origin: https://app.randomt.pt
Access-Control-Allow-Credentials: true  // necessário para cookies em pedidos cross-origin

O Mecanismo de Cookies em Detalhe

Um cookie é um par chave-valor que o servidor instrui o browser a guardar via o header Set-Cookie. A partir desse momento, o browser envia o cookie automaticamente em todos os pedidos para o mesmo domínio — sem que o JavaScript precise de fazer nada. Este comportamento automático é exactamente o que torna os cookies HttpOnly a escolha correcta para transportar o JWT.

// 1. LOGIN — o servidor autentica e define o cookie

// Pedido do cliente:
POST /api/auth/login HTTP/1.1
Content-Type: application/json
{"email":"ana@example.com","password":"secreta123"}

// Resposta do servidor após autenticação bem-sucedida:
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: access_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbmFAZXhhbXBsZS5jb20iLCJpZCI6OTl9.xyz;
            HttpOnly;       ← JavaScript não consegue ler document.cookie para este cookie
            Secure;         ← só enviado sobre HTTPS — nunca sobre HTTP
            SameSite=Strict;← só enviado em pedidos da mesma origem (protecção CSRF)
            Path=/;         ← válido para todos os paths do domínio
            Max-Age=900     ← expira em 900 segundos (15 minutos)

{"message": "Login successful"}

// O browser guarda o cookie internamente.
// O JavaScript vê apenas {"message": "Login successful"} — não vê o token.
// 2. PEDIDO SUBSEQUENTE — o browser envia o cookie automaticamente

// O utilizador navega para /dashboard — o browser faz um pedido à API:
GET /api/accounts HTTP/1.1
Host: app.randomt.pt
Accept: application/json
Cookie: access_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbmFAZXhhbXBsZS5jb20iLCJpZCI6OTl9.xyz
        ↑
        o browser anexou o cookie automaticamente
        o JavaScript não escreveu esta linha — o browser fê-lo por si próprio
        o JavaScript não consegue ler este valor — HttpOnly impede document.cookie

// O JwtAuthFilter no Spring Boot lê o cookie:
String jwt = Arrays.stream(request.getCookies())
    .filter(c -> "access_token".equals(c.getName()))
    .map(Cookie::getValue)
    .findFirst()
    .orElse(null);
// extrai o userId=99 do JWT e coloca no SecurityContext
// 3. LOGOUT — o servidor instrui o browser a apagar o cookie

// O servidor responde ao pedido de logout com um cookie de expiração imediata:
HTTP/1.1 200 OK
Set-Cookie: access_token=;
            HttpOnly; Secure; SameSite=Strict; Path=/;
            Max-Age=0    ← Max-Age=0 instrui o browser a apagar o cookie imediatamente

// O browser apaga o cookie.
// O JavaScript não precisa de fazer nada — não tinha acesso ao cookie de qualquer forma.
HttpOnly não é encriptação HttpOnly impede o JavaScript de ler o cookie via document.cookie — mas o cookie ainda viaja no header HTTP em texto claro se não houver TLS. É por isso que HttpOnly e Secure são sempre usados em conjunto: HttpOnly protege contra XSS, Secure protege contra intercepção em trânsito. Os dois flags são complementares, não alternativos.

Anatomia de uma Resposta HTTP

Uma resposta HTTP tem a mesma estrutura de um pedido: linha de início com o status code, headers, linha vazia, e body opcional.

// Resposta HTTP completa

HTTP/1.1 201 Created                   ← linha de início: versão + status code + reason phrase
Content-Type: application/json         ← formato do body
Content-Length: 156                    ← tamanho do body
Location: /api/accounts/43            ← URL do recurso criado (convenção para 201)
                                       ← linha vazia
{                                      ← body — o DTO de resposta serializado pelo Jackson
  "id": 43,
  "ownerName": "Ana Silva",
  "balance": 1500.0,
  "type": "SAVINGS",
  "status": "ACTIVE",
  "createdAt": "2026-06-01T10:30:00Z"
}

// Nota o que NÃO está na resposta:
// ✅ id, ownerName, balance, type, status, createdAt — dados públicos do recurso
// ❌ ownerId — campo interno de isolamento, nunca exposto (ver Módulo Java OOP)
// ❌ passwordHash — campo de segurança, nunca exposto
// ❌ internalStatus — detalhe de implementação interna

Status Codes: a Semântica da Resposta

O status code comunica o resultado do pedido de forma padronizada — o cliente sabe o que aconteceu sem ter de analisar o body. Usar os códigos correctos é parte do contrato de uma API bem desenhada.

CódigoSignificadoQuando usar em Spring Boot
200 OK Sucesso com body GET bem-sucedido. Retorna o recurso.
201 Created Recurso criado POST bem-sucedido. Incluir header Location com URL do novo recurso.
204 No Content Sucesso sem body DELETE bem-sucedido. PUT/PATCH sem necessidade de retornar o recurso.
400 Bad Request Pedido malformado Validação falhou (@Valid), JSON inválido, parâmetros em falta.
401 Unauthorized Não autenticado Sem cookie, cookie expirado, JWT inválido.
403 Forbidden Autenticado, sem permissão Role insuficiente. Nunca para recursos alheios — usar 404.
404 Not Found Recurso não existe (ou não é teu) Recurso inexistente e recurso que existe mas pertence a outro utilizador — isolamento lógico anti-IDOR.
409 Conflict Conflito de estado Email já registado. Operação impossível no estado actual.
422 Unprocessable Entity Semântica inválida Dados bem formados mas semanticamente inválidos (ex: data no passado obrigatoriamente futura).
500 Internal Server Error Erro não tratado Excepção inesperada apanhada pelo GlobalExceptionHandler. Mensagem genérica — sem detalhes internos.

O Fluxo Completo de Autenticação via Cookie

Com a anatomia de pedido e resposta clara, o fluxo completo da arquitectura de segurança deste projecto torna-se legível ao nível do protocolo HTTP.

// ════════════════════════════════════════════════════
// PASSO 1: Login
// ════════════════════════════════════════════════════

→ POST /api/auth/login
  Content-Type: application/json
  {"email":"ana@example.com","password":"secreta123"}

← 200 OK
  Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Max-Age=900
  {"message":"Login successful"}

// O browser guarda o cookie. O JavaScript não vê o JWT.

// ════════════════════════════════════════════════════
// PASSO 2: Pedido autenticado
// ════════════════════════════════════════════════════

→ GET /api/accounts/42
  Cookie: access_token=eyJ...     ← browser anexa automaticamente
  Accept: application/json

  [JwtAuthFilter]
    1. lê Cookie: access_token=eyJ...
    2. valida assinatura JWT
    3. extrai sub="ana@example.com", id=99
    4. coloca no SecurityContext

  [AccountController]
    Long userId = ((AppUserDetails) user).getId(); // → 99

  [AccountService]
    repository.findByIdAndOwnerId(42, 99)

  [AccountRepository]
    SELECT * FROM accounts WHERE id = 42 AND owner_id = 99
    → encontra: a conta 42 pertence ao utilizador 99 ✅

← 200 OK
  Content-Type: application/json
  {"id":42,"ownerName":"Ana Silva","balance":1500.0,...}

// ════════════════════════════════════════════════════
// PASSO 3: Tentativa de IDOR — aceder à conta de outro
// ════════════════════════════════════════════════════

→ GET /api/accounts/99    ← atacante tenta aceder à conta 99
  Cookie: access_token=eyJ...  (JWT válido, mas userId=99 no token)

  [AccountRepository]
    SELECT * FROM accounts WHERE id = 99 AND owner_id = 99
    → a conta 99 não pertence ao utilizador 99 → 0 resultados

  [AccountService]
    Optional.empty() → ResourceNotFoundException("Account not found")

  [GlobalExceptionHandler]
    ResourceNotFoundException → 404 Not Found

← 404 Not Found
  {"status":404,"error":"Not Found","message":"Account not found"}

// O atacante não sabe se a conta 99 existe ou não.
// A resposta é indistinguível de um recurso genuinamente inexistente.

// ════════════════════════════════════════════════════
// PASSO 4: Logout
// ════════════════════════════════════════════════════

→ POST /api/auth/logout
  Cookie: access_token=eyJ...

← 200 OK
  Set-Cookie: access_token=; HttpOnly; Secure; SameSite=Strict; Max-Age=0
  {"message":"Logged out"}

// Max-Age=0 instrui o browser a apagar o cookie imediatamente.
// Pedidos subsequentes chegam sem cookie → 401 Unauthorized.

Headers de Segurança na Resposta

Para além do Set-Cookie, uma API Spring Boot bem configurada deve incluir um conjunto de headers de segurança que instruem o browser a aplicar protecções adicionais.

// Configurar headers de segurança no Spring Boot
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .headers(headers -> headers
            // HSTS — forçar HTTPS mesmo na primeira visita
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000)  // 1 ano
            )
            // Impedir embedding em iframe (protecção contra clickjacking)
            .frameOptions(frame -> frame.deny())
            // Impedir browser de "adivinhar" o Content-Type
            .contentTypeOptions(Customizer.withDefaults())
            // Content Security Policy — restringir origens de recursos
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'")
            )
        )
        // ... resto da configuração
        .build();
}

// Resultado nos headers de resposta:
// Strict-Transport-Security: max-age=31536000; includeSubDomains
// X-Frame-Options: DENY
// X-Content-Type-Options: nosniff
// Content-Security-Policy: default-src 'self'; script-src 'self'

Checklist Request & Response