Sessão no servidor ou JWT: onde o token stateless deixa de compensar

Escrito por

em

Um usuário clica em “encerrar sessão em todos os dispositivos” e continua autenticado por mais quarenta minutos. O suporte abre chamado, alguém vai no código e descobre que sessão nenhuma existe do lado do servidor: o que existe é um JWT assinado com expiração de uma hora e nenhum lugar onde marcá-lo como inválido. A escolha entre sessão no servidor e token stateless raramente é sobre desempenho. É sobre quem detém a autoridade para encerrar um acesso antes da hora.

O que cada modelo guarda, e onde

Na sessão no servidor, o cliente recebe um identificador opaco — uma string aleatória sem significado próprio — dentro de um cookie HttpOnly. Todo o estado relevante (id do usuário, tenant, papéis, timestamp de emissão, fingerprint do dispositivo) fica num Redis, num Postgres ou onde o time preferir. A cada requisição o servidor faz uma leitura por chave para descobrir quem está do outro lado.

No JWT, a ordem se inverte. O token carrega as próprias claims em Base64URL e uma assinatura no fim: header.payload.signature. O servidor não consulta nada — verifica a assinatura com uma chave que já tem em memória e confia no conteúdo. O estado mora no cliente.

Duas consequências dessa inversão são frequentemente ignoradas em revisão de código. A primeira: o payload não é criptografado, só assinado. Base64URL é codificação, não proteção. E-mail, plano contratado, id interno de tenant e papel do usuário ficam legíveis para qualquer pessoa que copie o token do DevTools. A segunda: como não há consulta, também não há ponto onde interceptar. Um token emitido às 9h com exp às 10h vale até as 10h, tenha o usuário sido demitido, tido o plano cancelado ou perdido o notebook às 9h05.

Revogação é o eixo real da decisão

Com sessão no servidor, revogar é apagar um registro. DEL session:9f3a... e a próxima requisição daquele cookie falha. Encerrar todas as sessões de um usuário é apagar um conjunto. O custo é uma leitura por requisição, e essa leitura é exatamente o que dá ao servidor o poder de dizer não.

Com JWT, as saídas conhecidas são três, e nenhuma sai de graça.

Expiração curta. Reduzir exp para 5 ou 10 minutos encolhe a janela de exposição, mas transfere o problema para o refresh: alguma coisa precisa emitir o próximo token, e essa coisa consulta um estado persistido. O modelo stateless volta a ter estado, só que num ponto menos óbvio.

Denylist de tokens revogados. Funciona, e é a opção mais escolhida. Também é a que merece mais atenção: se cada requisição precisa checar se o jti está numa lista negra, o sistema faz uma leitura por requisição para autenticar — que é precisamente o custo da sessão no servidor, agora com assinatura criptográfica e complexidade extra por cima. Vale a pena quando a denylist é pequena e rara. Não vale quando revogação é evento cotidiano.

Rotação da chave de assinatura. Invalida tudo de uma vez. Serve para incidente de segurança, não para um usuário trocando de senha.

Uma medida útil antes de escolher: registre o tempo entre o pedido de revogação e a primeira requisição negada. SELECT max(revoked_at - requested_at) FROM audit_revocations WHERE requested_at > now() - interval '30 days'. Se o número passa de alguns segundos e o produto lida com dado sensível, a discussão já tem resposta.

O que cada modelo custa em infraestrutura

A conta de latência costuma ser apresentada como se fosse decisiva, e quase nunca é. Uma leitura em Redis na mesma rede fica na casa de dezenas a poucas centenas de microssegundos. A verificação de assinatura HS256 fica abaixo disso; RS256 e EdDSA custam mais que HMAC, mas ainda na mesma ordem de grandeza. Numa requisição que termina consultando o banco de dados da aplicação, essa diferença desaparece no ruído.

Dois custos menos citados importam mais.

O primeiro é tamanho na rede. Um JWT com claims de tenant, papéis e permissões chega facilmente a algumas centenas de bytes, às vezes ultrapassa o quilobyte quando alguém decide embutir a lista de permissões inteira. Esse peso viaja em toda requisição, inclusive nas de asset e polling. Proxies reversos têm limite de tamanho de header — o padrão do Nginx fica em poucos kilobytes por header — e o sintoma clássico é um 431 que só aparece para o usuário com mais permissões da base.

O segundo é operação. Sessão no servidor adiciona um componente com estado ao caminho crítico de autenticação: se o Redis cai, ninguém entra. Isso exige replicação, política de persistência decidida de forma consciente e um plano para o que acontece quando o cache é limpo (todos deslogam ao mesmo tempo, o que gera um pico de login que precisa caber na capacidade). JWT dispensa esse componente, e é aí que ele realmente ganha.

Há ainda um custo que aparece no front-end e é debitado da conta de segurança. Sessão em cookie HttpOnly é inacessível a JavaScript por construção: um XSS na aplicação continua sendo grave, mas não entrega o identificador de sessão de bandeja. JWT guardado em localStorage — prática comum em SPA por ser o caminho de menor atrito — é legível por qualquer script que execute na página, incluindo uma dependência transitiva comprometida. Guardar o JWT em cookie HttpOnly resolve esse ponto, mas devolve o problema de CSRF que o header Authorization evitava, e obriga a decidir entre SameSite=Lax e token anti-CSRF explícito. Vale registrar: quem escolhe JWT por segurança e o coloca em localStorage costuma terminar com uma superfície maior do que a sessão que substituiu.

Onde cada modelo ganha

Critério Sessão no servidor JWT stateless
Revogação imediata Nativa, uma exclusão Só com denylist ou janela de expiração
Dependência de armazenamento Obrigatória no caminho crítico Nenhuma para validar
Tamanho por requisição Dezenas de bytes Centenas de bytes a alguns KB
Validação fora da aplicação (edge, gateway) Exige consulta ao backend Local, só com a chave pública
Exposição de dados no cliente Nada legível Payload legível por quem tiver o token
Sessão simultânea e limite de dispositivos Consulta direta ao conjunto Precisa de registro paralelo
Auditoria de acesso ativo Lista disponível a qualquer momento Só por inferência de logs

O padrão que se repete: JWT ganha onde a validação precisa acontecer longe de quem emitiu. Chamada entre serviços internos, autorização num API gateway ou numa função de edge, token de curta duração emitido por um provedor de identidade externo, API pública com escopos por integração. Nesses cenários, exigir round-trip ao backend de autenticação é justamente o que se quer evitar.

Sessão no servidor ganha em aplicação web com cookie, painel administrativo, produto B2B com controle de acesso por administrador de conta e qualquer domínio em que “tirar alguém de dentro agora” seja requisito e não desejo — financeiro, saúde, RH, prontuário, folha.

A recomendação, e o que fazer com o que já existe

Para um SaaS web multi-tenant começando hoje: refresh token opaco em cookie HttpOnly, Secure e SameSite=Lax, com registro persistido por dispositivo, mais access token JWT de vida curta — 5 a 15 minutos — usado para autorizar as chamadas. Revogar significa apagar o registro do refresh; a janela residual é o tempo que sobra do access token, e essa janela é o preço explícito da escolha, não uma surpresa descoberta em incidente.

Quinze minutos de janela é aceitável para a maioria dos produtos. Não é aceitável quando a ação revogável tem efeito financeiro irreversível. Nesse caso a saída não é encurtar mais o token: é validar a autorização crítica contra o estado atual no momento da operação, independentemente do que o token afirma. Token diz quem é; a operação sensível confere se ainda pode.

Para quem já tem JWT puro em produção e sente a dor da revogação, a migração não precisa ser um big bang. Introduzir o endpoint de refresh com registro persistido, encurtar o exp do access token em etapas e só depois ligar a revogação real permite reverter em qualquer ponto. O indicador que fecha a migração é o mesmo que a justificou: a janela máxima entre pedido de revogação e negação efetiva, medida em produção, não estimada em reunião.

Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *