JWT Stateless Architecture Security
Módulo 1 de 3

JWT como Unidade de Identidade

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.

O Que é, Rigorosamente, um JWT

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
)
O payload é visível — nunca é secreto O payload é codificado em Base64URL, não cifrado. Nunca incluas passwords, dados de pagamento ou PII sensível num JWT JWS (assinado). Para confidencialidade real, usa JWE (JSON Web Encryption) — um padrão distinto.

Stateless: O Benefício Real

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.

Stateful · Sessões
Sessão Guardada no Servidor

Servidor mantém estado por utilizador. Escala horizontal exige store partilhado (Redis, BD). Revogação instantânea: trivial — apaga a entrada.

Stateless · JWT
Token Auto-Contido

Servidor valida assinatura sem consultar store. Escala horizontal nativa, sem acoplamento. Revogação antes da expiração: requer estratégia adicional.

O Trade-off Central: Escala vs. Revogação

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.

Estratégias de Mitigação

Recomendação Arquitetural A maioria das aplicações resolve com access tokens de 15 min + refresh tokens server-side de 7–30 dias. Blacklists só fazem sentido quando o risco ou a regulação o exigem explicitamente.

Fluxo com Access + Refresh Token

1
Login

Servidor valida credenciais e emite access_token (15 min) e refresh_token (7 dias) via cookies HttpOnly; Secure — nunca legíveis pelo JavaScript.

2
Pedidos normais

Browser envia o access_token automaticamente. Servidor valida assinatura e extrai claims sem tocar em qualquer store externo.

3
Access token expirado → 401

Servidor devolve 401. O frontend (interceptor Axios/fetch) deteta e dispara automaticamente POST /auth/refresh com o refresh token no cookie.

4
Renovação validada server-side

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.

5
Retry transparente

Pedido original é repetido com o novo access token. O utilizador nunca percebe a renovação — a experiência é contínua.

Implementação em Spring Boot

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());
}

Claims Recomendados e Proibidos

ClaimUsoRecomendação
subUser ID (subject)✅ Obrigatório. Identificador canónico e imutável.
role / scopePermissões de alto nível✅ Adequado. Usar roles estáticas, não permissões individuais mutáveis.
exp / iatExpiração e emissão✅ Obrigatório. Sem exp, o token é válido para sempre.
jtiJWT ID único⚠️ Necessário apenas se usar blacklist de tokens.
emailEndereç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.
O token informa — a base de dados decide O JWT diz "utilizador 99, role USER". Se tem acesso ao recurso 6 especificamente? Isso é responsabilidade da query SQL com owner_id = 99 — não do token. Esta separação de responsabilidades é o núcleo do Módulo 3.