JWT vs sessão: qual modelo de autenticação usar
Sessão dá controle total e revogação instantânea; JWT escala sem store central, mas revogar é o ponto fraco. Compare os trade-offs e escolha o modelo certo.
Toda vez que um app precisa lembrar quem está logado, alguém abre a discussão: "usa JWT ou sessão?". A pergunta vira religião rápido — tem quem ache sessão coisa de dinossauro e quem trate JWT como bala de prata. Os dois estão errados. São modelos diferentes, com trade-offs concretos, e a escolha errada custa caro: ou você reinventa sessão de um jeito pior, ou cria um sistema que não consegue deslogar ninguém. Este texto compara os dois pelo que importa de verdade — revogação, escala, onde guardar o token e segurança — e termina com uma recomendação direta.
O foco aqui é autenticação de aplicações web e APIs. Não vou cobrir OAuth, OIDC nem fluxo de terceiros; isso é assunto separado.
O modelo de sessão: estado no servidor
Sessão é o modelo clássico. No login, o servidor gera um session id aleatório, guarda os dados associados (quem é o usuário, permissões, o que for) em um store — memória, Redis, banco — e devolve só o id para o cliente, normalmente dentro de um cookie. Em cada request seguinte, o navegador manda o cookie automaticamente, o servidor pega o id, busca os dados no store e sabe quem está falando.
O ponto forte é o controle total. Quer deslogar um usuário agora? Apaga a linha no store. Quer derrubar todas as sessões de alguém depois de uma troca de senha? Apaga todas as entradas daquele usuário. Quer mudar permissões e que valham no próximo request? É só editar o dado no servidor. Nada disso depende de o cliente cooperar. O cookie carrega um id opaco e sem valor por si só — vazou, você revoga.
O preço aparece quando você escala horizontalmente. Se a sessão vive na memória de um processo, o request do usuário tem que cair sempre no mesmo servidor (sticky sessions) ou ele perde o login ao trocar de instância. A solução padrão é um store compartilhado — Redis na frente de todos os servidores. Funciona muito bem, é barato e previsível, mas é mais uma peça de infraestrutura para manter, monitorar e que vira ponto único de falha se você não cuidar.
O modelo JWT: stateless de verdade
JWT (JSON Web Token) inverte a lógica. Em vez de guardar o estado no servidor, ele coloca os dados — os claims — dentro do próprio token, que vai assinado. Um JWT tem três partes separadas por ponto: header, payload e signature.
header.payload.signature
eyJhbGciOiJI... . eyJzdWIiOiIxMjM0... . SflKxwRJSMeKKF2QT4...
O payload guarda coisas como sub (o usuário), exp (quando expira) e o que você quiser. A assinatura é gerada com uma chave secreta (ou par de chaves) e garante que ninguém alterou o conteúdo. No request seguinte, o servidor só precisa verificar a assinatura com a chave — sem ir a lugar nenhum buscar nada. É aí que está o ganho: não existe store central, não existe lookup. Qualquer servidor que tenha a chave consegue validar o token sozinho.
Isso brilha em dois cenários. APIs sem estado, onde você não quer carregar infraestrutura de sessão, e comunicação entre serviços (service-to-service), onde um microserviço passa o token adiante e cada serviço valida por conta própria. Escala sem dor de cabeça de store compartilhado.
Vale notar que o payload é só base64, não é criptografia — qualquer um lê o conteúdo. Você pode decodificar um token para ver os claims sem mandar nada a servidor nenhum, justamente porque os dados estão ali em texto, só assinados. Não coloque segredo dentro de um JWT.
Revogação: o calcanhar do JWT
Aqui mora o problema honesto do JWT, e é o que mais gente ignora na pressa de adotar. Como o servidor valida o token só pela assinatura, sem consultar nada, ele não tem como saber que aquele token "não vale mais". Trocou a senha? O token antigo continua válido até o exp. Demitiu o funcionário? O token dele funciona até expirar. Detectou que vazou? Não dá para invalidar antes da hora.
A saída usual é manter uma blocklist — uma lista de tokens revogados que o servidor consulta a cada request. Funciona, mas perceba a ironia: você acabou de adicionar um lookup em store central a cada request. Ou seja, matou o "stateless" que era o argumento inteiro a favor do JWT. Você reconstruiu a sessão, só que com mais bytes trafegando e mais complexidade.
A mitigação real é usar exp curto. Um access token que vive 5 a 15 minutos limita a janela de estrago: mesmo sem revogar, o dano expira sozinho rápido. Mas tokens curtos exigem renovação frequente, e é aí que entram os refresh tokens.
Refresh tokens, tamanho e onde guardar
O padrão maduro com JWT é dois tokens. Um access token curto (minutos) que vai em toda request, e um refresh token longo (dias) que serve só para obter um novo access token quando ele expira. O refresh token você guarda no servidor — sim, com estado — e pode revogar. Repare que você voltou a ter um store; a diferença é que ele só é consultado na renovação, não em todo request. É um meio-termo razoável.
Onde guardar do lado do cliente é onde mais gente erra. A resposta certa é um cookie httpOnly, Secure e SameSite. httpOnly impede que JavaScript leia o token, o que corta o roubo via XSS. O erro clássico é jogar o JWT em localStorage "porque é prático": qualquer script injetado lê o token e ele vaza. Cookie httpOnly vale tanto para session id quanto para JWT — não é vantagem de nenhum dos dois, é higiene básica.
Tem também o tamanho. Um session id são poucos bytes. Um JWT com vários claims pode passar de 1 KB, e ele viaja em toda request. Em alto volume, isso soma banda e latência. Não é decisivo na maioria dos casos, mas é real.
A recomendação direta
Para um app web monolítico, comece com sessão e cookie httpOnly. É mais simples, é mais seguro do que a maioria imagina e te dá revogação de graça. Você não precisa de refresh token, não precisa de blocklist, não precisa decidir tempo de expiração com medo. Adicionar Redis quando escalar é trivial e resolvido há décadas.
Não comece com JWT por moda. JWT é a escolha certa quando você tem APIs sem estado de verdade, comunicação entre múltiplos serviços, ou um cenário onde o store central de sessão é o gargalo que você está tentando eliminar. Nesses casos ele é excelente. Fora deles, você normalmente está pagando a complexidade do stateless sem colher o benefício — e o pior contra-senso do mercado é usar JWT em localStorage num app web comum e, na prática, reimplementar sessão de um jeito pior e menos seguro.
Se quiser entender a mecânica de assinatura, claims e verificação por dentro antes de decidir, veja como funciona um JWT por dentro.
Perguntas frequentes
JWT é mais seguro que sessão?
Não, não por natureza. Segurança vem de onde você guarda o token (cookie httpOnly em ambos), do uso de HTTPS e de boa gestão de expiração e revogação. Sessão até leva vantagem em um ponto: revogar é instantâneo. JWT mal usado, em localStorage e com exp longo, é menos seguro que uma sessão bem feita.
Posso revogar um JWT antes de ele expirar?
Não sem abrir mão do stateless. A única forma é manter uma blocklist no servidor e consultá-la a cada request, o que recria a dependência de store central que o JWT prometia evitar. A prática comum é usar exp curto (5 a 15 minutos) com refresh token, aceitando uma janela pequena em que o token continua válido.
Onde devo guardar o token no navegador?
Em um cookie com as flags httpOnly, Secure e SameSite. Isso vale tanto para session id quanto para JWT. localStorage é uma má ideia porque qualquer JavaScript da página — inclusive um injetado via XSS — consegue ler o conteúdo e roubar o token.
Quando JWT realmente compensa?
Em APIs stateless, em arquitetura de microserviços onde o token é repassado entre serviços, e quando o store central de sessão virou gargalo de escala. Nesses contextos o ganho de não ter lookup a cada request é real. Para um app web monolítico, sessão costuma ser a escolha mais simples e segura.
O que levar daqui
Sessão e JWT não competem pelo mesmo lugar. Sessão é estado no servidor: controle total, revogação instantânea, custo de um store compartilhado ao escalar. JWT é stateless: escala sem store central e é ótimo entre serviços, ao preço de revogação difícil e tokens maiores. Escolha pelo problema, não pela moda — monolito web pede sessão, API distribuída pede JWT — e, em qualquer dos dois, guarde o token num cookie httpOnly.
- 01 Salt, pepper, bcrypt e Argon2id: como proteger senhas de verdade Em 2012, o LinkedIn expôs 117mi de senhas. SHA-1 sem salt — 90% quebradas em 4h. Entenda o que cada camada de proteção resolve e por que Argon2id é a escolha certa hoje.
- 02 Licenças MIT, Apache e GPL: diferenças práticas para devs MIT, Apache 2.0 e GPL não são a mesma coisa. Entenda permissiva vs copyleft, proteção de patentes e compatibilidade antes de colocar código em produção.