Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP8 min
Sessão vs JWT
- Sessão em servidor guarda estado; JWT carrega estado. Qual o custo de cada escolha?
- Como você revoga um JWT antes de ele expirar? (é a pergunta difícil)
- Onde guardar o token no cliente, e o que cada lugar expõe (XSS vs CSRF)?
- O que refresh token resolve, e que novo problema ele cria?
Conceito
As duas respostas para "como o servidor sabe quem você é", dado que HTTP não guarda estado (1. Modelo Cliente-Servidor):
| Sessão | JWT | |
|---|---|---|
| Quem guarda o estado | o servidor (store) | o cliente (no token) |
| O que o cliente carrega | um id opaco | os dados + assinatura |
| Verificação por requisição | consulta ao store | verificar assinatura (só CPU) |
| Revogação | imediata (apague o registro) | impossível por design |
| Escala horizontal | precisa de store compartilhado | não precisa de nada |
| Tamanho no fio | ~32 bytes | centenas de bytes a KB |
A troca é clara: sessão paga uma consulta por requisição e ganha revogação imediata; JWT não paga consulta e perde a revogação.
Você não pode. O token é autocontido e válido enquanto a assinatura confere e o exp
não passou. O servidor não tem onde marcar "este não vale mais".
Todas as respostas na prática reintroduzem estado — ou seja, viram uma sessão com passos extras:
| Solução | O que ela é de fato |
|---|---|
exp curto (5–15 min) + refresh token |
uma sessão, para o refresh token |
| denylist de tokens revogados | um store de sessão, invertido |
| versão do token no banco, checada por requisição | uma consulta por requisição |
Conclusão honesta: se você precisa de revogação, você precisa de estado. O JWT apenas muda onde ele fica. Escolher JWT "para não ter estado" e depois adicionar denylist é escolher o pior dos dois.
Onde guardar no cliente, e a troca é XSS contra CSRF:
| Local | Vulnerável a | Observação |
|---|---|---|
localStorage |
XSS (JS lê e exfiltra) | conveniente, e o pior caso é total |
cookie HttpOnly Secure SameSite |
CSRF (JS não lê, mas o navegador envia) | XSS não consegue ler o token |
| memória (variável JS) | perdido em cada recarga | melhor postura, pior UX |
O consenso atual: cookie HttpOnly + Secure + SameSite=Lax é a melhor escolha padrão,
porque XSS deixa de conseguir roubar a credencial. CSRF, o problema restante, é mitigado
por SameSite mais um token anti-CSRF onde necessário.
Refresh token resolve o problema criado pelo exp curto (o usuário seria deslogado a cada
15 minutos) e cria três novos: uma credencial de vida longa para proteger, a necessidade de
rotação (cada uso emite um novo), e detecção de reuso (se um refresh já usado aparece
de novo, alguém o roubou — invalide a família inteira).
Na prática
Verificado: o payload de um JWT é legível sem chave nenhuma.
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
payload decodificado SEM chave nenhuma: map[exp:1.8e+09 role:admin sub:42]
Isso é base64url, não criptografia. JWT é assinado (integridade e autenticidade),
não cifrado (confidencialidade).
Consequência prática direta: nunca coloque dado sensível num JWT. CPF, e-mail, papel interno, id de outro sistema — tudo isso vai visível para o cliente e para qualquer coisa que logue o token. (Existe JWE para cifrar, e quase ninguém usa.)
Em Go
Não há JWT na biblioteca padrão — a escolha é golang-jwt/jwt/v5. Mas os blocos
existem: crypto/hmac, crypto/sha256, encoding/base64.
1. alg: none. O atacante troca o header para {"alg":"none"}, remove a assinatura e o
servidor aceita. Bibliotecas antigas caíam nisso.
2. Confusão de algoritmo (HS256 vs RS256). O servidor usa RSA (chave pública conhecida).
O atacante troca alg para HS256 e assina com a chave pública como se fosse segredo
HMAC. Se o código escolhe o algoritmo pelo header do token, ele valida.
A defesa, em qualquer linguagem: nunca confie no alg do token. Fixe o algoritmo
esperado na validação:
tok, err := jwt.Parse(s, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // FIXA o método
return nil, fmt.Errorf("alg inesperado: %v", t.Header["alg"])
}
return chaveSecreta, nil
}, jwt.WithValidMethods([]string{"HS256"})) // e/ou declare aqui
Cookie de sessão correto em Go:
http.SetCookie(w, &http.Cookie{
Name: "sid",
Value: idAleatorio, // crypto/rand, nunca math/rand
Path: "/",
HttpOnly: true, // JS não lê: mitiga XSS
Secure: true, // só sobre TLS
SameSite: http.SameSiteLaxMode, // mitiga CSRF
MaxAge: int(8 * time.Hour / time.Second),
})
O id de sessão precisa vir de crypto/rand, não de math/rand:
b := make([]byte, 32)
rand.Read(b) // crypto/rand
id := base64.RawURLEncoding.EncodeToString(b)
math/rand é previsível — com a semente, um atacante gera os ids dos outros.
Comparação de token em tempo constante: ao comparar segredos, use
subtle.ConstantTimeCompare em vez de ==, para não vazar informação por tempo de resposta.
Respostas às perguntas-guia
1. Sessão em servidor guarda estado; JWT carrega estado. Qual o custo de cada escolha?
Sessão: uma consulta ao store por requisição, e um store para operar — em troca de revogação imediata. JWT: nenhuma consulta e escala trivial — em troca de não poder revogar e de tráfego maior.
2. Como você revoga um JWT antes de ele expirar?
Não revoga. As três soluções (exp curto + refresh, denylist, versão no banco) todas reintroduzem estado. Se revogação é requisito, sessão é mais honesta.
Em Go: a denylist costuma virar Redis com TTL igual ao exp restante — que é literalmente
um store de sessão.
3. Onde guardar o token no cliente, e o que cada lugar expõe (XSS vs CSRF)?
localStorage expõe a XSS (roubo total). Cookie HttpOnly expõe a CSRF (uso indevido, sem
roubo). Memória é a mais segura e perde na recarga.
Em Go: HttpOnly + Secure + SameSite=Lax como padrão, e SameSite=None só com
Secure quando o front está em outro domínio.
4. O que refresh token resolve, e que novo problema ele cria?
Resolve a expiração curta do access token sem deslogar o usuário. Cria: uma credencial de vida longa para proteger, a necessidade de rotação a cada uso, e detecção de reuso (reuso = roubo → invalide a família).
Trade-offs
Do conceito:
- Sessão compra revogação e controle; cobra store compartilhado e uma consulta por requisição.
- JWT compra ausência de consulta e escala; cobra revogação e tráfego.
expcurto compra janela pequena de dano e cobra refresh (mais estado).- Cookie
HttpOnlycompra proteção contra roubo por XSS e cobra lidar com CSRF.
Em Go:
- Sem JWT na stdlib: uma dependência, e uma decisão de segurança na sua mão (fixar o
alg). - Sessão à mão é ~50 linhas com Redis, e você entende tudo.
crypto/randesubtle.ConstantTimeCompareestão na stdlib — não há motivo para errar esses dois.
Exemplo prático
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// ============ JWT à mão, com stdlib, para ver o que ele É ============
func assinar(claims map[string]any, segredo []byte) string {
hdr, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
pay, _ := json.Marshal(claims)
corpo := base64.RawURLEncoding.EncodeToString(hdr) + "." +
base64.RawURLEncoding.EncodeToString(pay)
m := hmac.New(sha256.New, segredo)
m.Write([]byte(corpo))
return corpo + "." + base64.RawURLEncoding.EncodeToString(m.Sum(nil))
}
// Validação CORRETA: o algoritmo é FIXO no código, não lido do token.
func validar(token string, segredo []byte) (map[string]any, error) {
partes := strings.Split(token, ".")
if len(partes) != 3 {
return nil, fmt.Errorf("formato inválido")
}
// 1. o alg do header NÃO decide nada — só é conferido
hdrBytes, err := base64.RawURLEncoding.DecodeString(partes[0])
if err != nil {
return nil, err
}
var hdr struct{ Alg string }
json.Unmarshal(hdrBytes, &hdr)
if hdr.Alg != "HS256" {
return nil, fmt.Errorf("alg inesperado: %q", hdr.Alg) // barra alg:none e RS256
}
// 2. recalcula a assinatura com o algoritmo FIXO
m := hmac.New(sha256.New, segredo)
m.Write([]byte(partes[0] + "." + partes[1]))
esperada := base64.RawURLEncoding.EncodeToString(m.Sum(nil))
if subtle.ConstantTimeCompare([]byte(esperada), []byte(partes[2])) != 1 {
return nil, fmt.Errorf("assinatura inválida")
}
// 3. só então confia nas claims
payBytes, _ := base64.RawURLEncoding.DecodeString(partes[1])
var claims map[string]any
json.Unmarshal(payBytes, &claims)
if exp, ok := claims["exp"].(float64); ok && time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("expirado")
}
return claims, nil
}
// ============ Sessão: id opaco + store ============
type sessao struct {
UsuarioID int
Papel string
Expira time.Time
}
type store struct{ dados map[string]sessao }
func novoID() string {
b := make([]byte, 32)
rand.Read(b) // crypto/rand, NUNCA math/rand
return base64.RawURLEncoding.EncodeToString(b)
}
func (s *store) cria(u int, papel string) string {
id := novoID()
s.dados[id] = sessao{u, papel, time.Now().Add(8 * time.Hour)}
return id
}
func (s *store) busca(id string) (sessao, bool) {
sess, ok := s.dados[id]
if !ok || time.Now().After(sess.Expira) {
return sessao{}, false
}
return sess, true
}
func (s *store) revoga(id string) { delete(s.dados, id) } // IMEDIATO
func main() {
segredo := []byte("segredo-de-producao-vem-de-variavel-de-ambiente")
// ---------- JWT ----------
tok := assinar(map[string]any{
"sub": "42",
"role": "admin",
"exp": time.Now().Add(15 * time.Minute).Unix(),
}, segredo)
fmt.Println("--- JWT ---")
fmt.Printf("token (%d bytes): %s...\n", len(tok), tok[:45])
// O payload é LEGÍVEL por qualquer um, sem chave:
pay, _ := base64.RawURLEncoding.DecodeString(strings.Split(tok, ".")[1])
fmt.Printf("payload lido SEM a chave: %s\n", pay)
fmt.Println("^ assinado (integridade), NAO cifrado (confidencialidade)")
c, err := validar(tok, segredo)
fmt.Printf("validação ok: %v %v\n", c["role"], err)
// adulterar o payload invalida a assinatura
p := strings.Split(tok, ".")
falso, _ := json.Marshal(map[string]any{"sub": "42", "role": "superadmin",
"exp": time.Now().Add(time.Hour).Unix()})
adulterado := p[0] + "." + base64.RawURLEncoding.EncodeToString(falso) + "." + p[2]
_, err = validar(adulterado, segredo)
fmt.Printf("payload adulterado: %v\n", err)
// ataque alg:none — barrado porque o alg é fixo no código
hdrNone, _ := json.Marshal(map[string]string{"alg": "none", "typ": "JWT"})
semAssinatura := base64.RawURLEncoding.EncodeToString(hdrNone) + "." + p[1] + "."
_, err = validar(semAssinatura, segredo)
fmt.Printf("ataque alg:none: %v\n", err)
// não há como revogar antes do exp
fmt.Println("revogar este token agora: impossível sem reintroduzir estado")
// ---------- Sessão ----------
fmt.Println("\n--- Sessão ---")
st := &store{dados: map[string]sessao{}}
sid := st.cria(42, "admin")
fmt.Printf("id opaco (%d bytes): %s...\n", len(sid), sid[:20])
fmt.Println("^ o cliente não descobre NADA a partir dele")
s1, ok := st.busca(sid)
fmt.Printf("consulta ao store: papel=%s ok=%v\n", s1.Papel, ok)
st.revoga(sid)
_, ok = st.busca(sid)
fmt.Printf("após revogar: ok=%v <- IMEDIATO\n", ok)
}
Relacionado
- 8. OAuth — de onde o token costuma vir
- 6. Headers HTTP —
Authorization,Set-Cookiee os atributos do cookie - 3. TLS — cookie
Securesó faz sentido sobre TLS - 1. Modelo Cliente-Servidor — as quatro opções de onde o estado mora
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação