Desconecte um usuário de uma aplicação baseada em sessão e o servidor o esquece na próxima requisição. Desconecte-o de uma aplicação baseada em JWT e, a menos que você tenha construído um mecanismo extra para isso, o token que ele está carregando continua válido até expirar. Essa diferença é a verdadeira decisão por trás de “JWT vs sessão”. O resto decorre dela.

O que uma sessão realmente guarda

Uma sessão mantém o estado do usuário no servidor e passa ao navegador apenas uma chave de consulta.

  1. O usuário faz login com uma senha.
  2. O servidor cria um registro de sessão (ID do usuário, papéis, o que for preciso) e o guarda em memória, em um banco de dados ou no Redis, indexado por um ID de sessão aleatório.
  3. O servidor envia esse ID ao navegador como cookie, marcado HttpOnly para que o JavaScript do lado do cliente não possa lê-lo.
  4. A cada requisição, o navegador devolve o cookie automaticamente. O servidor procura o ID no store e recupera os dados da sessão.

O cookie não carrega nenhuma informação do usuário, só uma string opaca que o servidor resolve a cada requisição. Apague o registro no store e a sessão desaparece na próxima requisição, em todos os dispositivos.

O que tem dentro de um JWT

Um JSON Web Token pula essa consulta colocando o estado direto no token, assinado para que não possa ser adulterado.

Um JWT é formado por três partes codificadas em base64url unidas por pontos: header.payload.signature. O payload é um objeto JSON de claims. A RFC 7519 define uma lista curta de claims registrados:

ClaimNomeSignificado
issIssuerQuem emitiu o token
subSubjectA quem o token se refere
audAudiencePara quem o token é destinado
expExpirationRejeitar o token depois deste momento
nbfNot beforeRejeitar o token antes deste momento
iatIssued atQuando o token foi criado
jtiJWT IDID único, útil para revogação e verificações de replay

O servidor assina o header e o payload com um secret (HMAC) ou uma chave privada (RSA/ECDSA) e envia o resultado ao cliente. Qualquer servidor que tenha a chave correspondente, ou apenas a chave pública na assinatura assimétrica, pode verificar a assinatura e confiar nos claims. Nenhuma ida e volta ao banco de dados, nenhum store de sessão compartilhado. Em um sistema com vários serviços, cada um pode verificar um token por conta própria.

O lado inverso: quem tiver o token pode ler o payload. Base64 é uma codificação, não uma criptografia. Nunca coloque uma senha, um secret ou dados pessoais que você não imprimiria em um log dentro do payload de um JWT.

Como configurar a autenticação por sessão no Express

Uma configuração típica no Node usa express-session como middleware e connect-redis para que as sessões sobrevivam a um reinício do servidor e funcionem em várias instâncias. Se o Redis é novo para você, como funciona o cache Redis cobre o básico para colocá-lo em funcionamento.

1
npm install express express-session connect-redis redis
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require("express");
const session = require("express-session");
const { RedisStore } = require("connect-redis");
const { createClient } = require("redis");

const redisClient = createClient({ url: "redis://localhost:6379" });
redisClient.connect().catch(console.error);

const app = express();
app.use(express.json());

app.use(
  session({
    store: new RedisStore({ client: redisClient, prefix: "sess:" }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 2, // 2 horas
    },
  })
);

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  req.session.userId = user.id;
  res.sendStatus(204);
});

app.get("/me", (req, res) => {
  if (!req.session.userId) return res.sendStatus(401);
  res.json({ userId: req.session.userId });
});

app.post("/logout", (req, res) => {
  req.session.destroy(() => res.sendStatus(204));
});

app.listen(3000);

req.session.destroy() remove a entrada no Redis. A próxima requisição com esse cookie não encontra nada e é tratada como desconectada. Nenhuma blacklist de tokens, nada para ajustar depois.

Como configurar a autenticação JWT no Node

O mesmo fluxo com jsonwebtoken, emitindo um access token de curta duração e um refresh token separado:

1
npm install express jsonwebtoken
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const crypto = require("crypto");
const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

function issueTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: "15m", algorithm: "HS256" }
  );
  const refreshToken = jwt.sign(
    { sub: user.id, jti: crypto.randomUUID() },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: "7d", algorithm: "HS256" }
  );
  return { accessToken, refreshToken };
}

function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.sendStatus(401);

  try {
    req.user = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ["HS256"],
    });
    next();
  } catch (err) {
    return res.sendStatus(401);
  }
}

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  res.json(issueTokens(user));
});

app.get("/me", requireAuth, (req, res) => {
  res.json({ userId: req.user.sub, role: req.user.role });
});

app.listen(3000);

jwt.verify checa a assinatura e os claims exp/nbf em uma única chamada e lança uma exceção se qualquer um dos dois falhar. Essa é toda a verificação de autenticação, sem nenhum store envolvido.

A opção algorithms: ["HS256"] importa. Sem ela, um token assinado com um algoritmo diferente do esperado ainda poderia passar, e é assim que várias bibliotecas JWT acabaram com vulnerabilidades de “alg confusion”. Fixe no código a lista de algoritmos aceitos na verificação e rejeite sempre alg: "none".

O refresh token é o que torna viável um access token de 15 minutos. O cliente o guarda e, quando o access token expira, troca por um novo par. Guarde o jti do refresh token no servidor para poder revogar só aquela cadeia sem tocar nos access tokens já em circulação.

Por que os JWTs são difíceis de revogar

Uma sessão é um registro em um store: apague-o e o usuário é desconectado em todo lugar. Um JWT é um claim autocontido que qualquer verificador aceita até que exp diga o contrário. Fazer logout, banir um usuário ou rebaixar seu papel não muda nada em um token já emitido, porque o servidor que verifica não tem nenhum registro para apagar.

As mitigações mais comuns:

  • Vida curta do access token. 15-30 minutos é a faixa mais usada, para que um token roubado ou velho tenha uma janela pequena.
  • Rotação do refresh token. Cada refresh emite um novo refresh token e invalida o antigo; reutilizar um refresh token antigo revoga toda a cadeia. O OAuth 2.1 exige isso (ou tokens sender-constrained) para clientes públicos.
  • Uma blacklist bem delimitada. Para matar um token específico antes de expirar, indexe a blacklist pelo jti mais o iss, não pela string crua do token. O OWASP JWT Cheat Sheet explica por que colocar a própria string na blacklist não é confiável. Uma blacklist checada em cada requisição também traz de volta uma consulta ao banco em cada requisição — exatamente o custo que os JWTs deveriam eliminar.

Se sua aplicação acaba fazendo uma consulta a cada verificação de qualquer forma, uma sessão teria sido o design mais simples.

Onde guardar um JWT no navegador

Em uma aplicação de navegador, coloque o token em um cookie HttpOnly e Secure, não no localStorage. Qualquer script rodando na página pode ler o localStorage, então uma única brecha de XSS entrega todos os tokens que estiverem lá. O custo: uma vez que o token viaja em um cookie, o navegador o envia automaticamente, e você volta a se defender de CSRF com SameSite e tokens CSRF, exatamente como nas sessões. A comparação completa está em LocalStorage, SessionStorage ou Cookies: Qual Usar.

Quando usar sessões e quando usar JWTs

SituaçãoUse
Uma única aplicação renderizada no servidor, um único backendSessões: mais simples, revogação imediata
Logout instantâneo ou mudanças de permissão instantâneasSessões, ou JWTs com uma blacklist por jti
Vários serviços de backend que verificam o usuário cada um por conta própriaJWTs: nenhum store de sessão compartilhado para consultar
App mobile ou clientes de API de terceirosJWTs: bearer tokens combinam com uma API HTTP sem estado

Nenhum dos dois é mais seguro em abstrato. Uma sessão no Redis e um JWT de 15 minutos com refresh tokens rotacionados se sustentam igualmente bem. A diferença é arquitetural: quantos serviços precisam confiar no claim e quão rápido você precisa conseguir retirá-lo.

Se você está construindo uma aplicação com um único backend, escolha sessões por padrão e ganhe a revogação de graça. Mude para JWTs no dia em que um segundo serviço precisar saber quem é o usuário sem chamar de volta o serviço que o autenticou.

Artigos relacionados