O JSON Web Token não é uma solução de sessão — é uma unidade de identidade matematicamente verificável. Este módulo explica o que o JWT transporta, os seus limites reais, o trade-off central de escala vs. revogação, e como integrá-lo corretamente numa arquitetura stateless de produção.
Um JWT (RFC 7519) é uma string Base64URL composta por três partes separadas por
pontos: header.payload.signature. Por omissão, não é cifrado —
é assinado. Qualquer um pode ler o conteúdo; apenas o detentor
da chave secreta consegue produzir uma assinatura válida.
// ── HEADER — algoritmo e tipo ───────────────────────────
{
"alg": "HS256", // algoritmo de assinatura
"typ": "JWT"
}
// ── PAYLOAD — claims do utilizador ──────────────────────
{
"sub": "99", // subject: user id canónico
"role": "USER", // claim de autorização
"scope": ["accounts:read", "accounts:write"],
"iat": 1748520000, // issued at (Unix timestamp)
"exp": 1748523600 // expira em 1 hora
}
// ── SIGNATURE ────────────────────────────────────────────
HMACSHA256(
base64url(header) + "." + base64url(payload),
SECRET_KEY
)
Numa arquitetura com sessões, o servidor mantém um mapa
session_id → dados do utilizador em memória ou num store externo.
Cada pedido exige uma consulta a esse store — um ponto de acoplamento obrigatório.
Com JWT, o servidor valida matematicamente a assinatura recebida e extrai os claims diretamente do token. Qualquer instância horizontal pode fazê-lo de forma independente — é aqui que está o benefício real de escalabilidade.
Servidor mantém estado por utilizador. Escala horizontal exige store partilhado (Redis, BD). Revogação instantânea: trivial — apaga a entrada.
Servidor valida assinatura sem consultar store. Escala horizontal nativa, sem acoplamento. Revogação antes da expiração: requer estratégia adicional.
O JWT resolve elegantemente a escala horizontal, mas cria tensão com a revogação imediata. Um token válido continua a ser aceite até expirar, mesmo após logout — a menos que exista uma estratégia de invalidação adicional.
Servidor valida credenciais e emite access_token (15 min)
e refresh_token (7 dias) via cookies HttpOnly; Secure
— nunca legíveis pelo JavaScript.
Browser envia o access_token automaticamente. Servidor valida
assinatura e extrai claims sem tocar em qualquer store externo.
Servidor devolve 401. O frontend (interceptor Axios/fetch) deteta e dispara
automaticamente POST /auth/refresh com o refresh token no cookie.
Servidor confirma o refresh token na BD — não expirado, não revogado. Emite novo par. É aqui que logout e banimento têm efeito real e imediato.
Pedido original é repetido com o novo access token. O utilizador nunca percebe a renovação — a experiência é contínua.
O filter intercepta cada pedido antes dos controllers, valida a assinatura e injeta
a identidade no SecurityContext. Nenhum controller precisa de
conhecer JWT diretamente.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
@Override
protected void doFilterInternal(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws Exception {
// Lê o JWT do cookie HttpOnly — nunca de um header Authorization
String token = Arrays.stream(
Optional.ofNullable(req.getCookies()).orElse(new Cookie[0]))
.filter(c -> "access_token".equals(c.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if (token == null) {
chain.doFilter(req, res);
return;
}
try {
Claims claims = jwtService.validate(token);
Long userId = Long.valueOf(claims.getSubject());
String role = claims.get("role", String.class);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
new AuthenticatedUser(userId, role),
null,
List.of(new SimpleGrantedAuthority("ROLE_" + role))
);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
chain.doFilter(req, res);
}
}
// Como extrair o userId nos controllers — zero acoplamento a JWT
@GetMapping("/accounts/{id}")
public ResponseEntity<Account> getAccount(
@PathVariable Long id,
@AuthenticationPrincipal AuthenticatedUser currentUser) {
// currentUser.getId() vem do JWT validado pelo filter — nunca do cliente
return accountService
.findByIdAndOwner(id, currentUser.getId())
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
| Claim | Uso | Recomendação |
|---|---|---|
sub | User ID (subject) | ✅ Obrigatório. Identificador canónico e imutável. |
role / scope | Permissões de alto nível | ✅ Adequado. Usar roles estáticas, não permissões individuais mutáveis. |
exp / iat | Expiração e emissão | ✅ Obrigatório. Sem exp, o token é válido para sempre. |
jti | JWT ID único | ⚠️ Necessário apenas se usar blacklist de tokens. |
email | Endereço de email | ⚠️ Evitar. PII desnecessária no token. |
password | — | 🚫 Nunca. Payload visível, não cifrado. |
| Dados de pagamento | — | 🚫 Nunca. Sem benefício em transportar no token. |
owner_id = 99 — não do token.
Esta separação de responsabilidades é o núcleo do Módulo 3.