Capítulo 10: Autenticação Stateless com JWT e Filtros Customizados
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Autenticação Stateless com JWT e Filtros Customizados"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Autenticação Stateless com JWT e Filtros Customizados
Por que JWT em vez de sessão. Em uma API stateless (Capítulo 09 já configurou SessionCreationPolicy.STATELESS), o servidor não guarda nenhum estado de autenticação entre requisições — não existe HttpSession nem armazenamento server-side do “quem está logado”. O JWT (JSON Web Token) resolve isso transportando a própria prova de identidade no lado do cliente: um token auto-contido, composto por três partes separadas por ponto (header.payload.signature), codificadas em Base64URL. O payload carrega claims — sub (subject/usuário), iat (issued at), exp (expiration), papéis/roles — e a signature garante que ninguém alterou esse conteúdo sem possuir a chave secreta.
Geração do token. A biblioteca jjwt (io.jsonwebtoken) constrói o token com uma API fluente: Jwts.builder().subject(...).issuedAt(...).expiration(...).signWith(chave).compact(). A assinatura pode ser simétrica (HMAC-SHA256, uma única chave secreta compartilhada entre quem emite e quem valida — mais simples, mas exige que o segredo circule entre serviços) ou assimétrica (RSA/EC, par de chaves pública/privada — o emissor assina com a privada, qualquer serviço valida com a pública, sem nunca compartilhar segredo; é o modelo usado por OAuth2/OpenID Connect via JWKS).
O filtro de autenticação por requisição. Como não há sessão, cada requisição precisa provar sua identidade novamente — isso é feito por um filtro Servlet customizado que herda de OncePerRequestFilter, garantindo execução única por requisição mesmo em cenários de forward/include internos. O filtro é registrado na SecurityFilterChain antes do UsernamePasswordAuthenticationFilter padrão (via addFilterBefore), intercepta o header Authorization: Bearer <token>, valida assinatura e expiração e, se tudo estiver correto, monta um UsernamePasswordAuthenticationToken com o usuário e suas GrantedAuthoritys, registrando-o em SecurityContextHolder.getContext().setAuthentication(...). Só então a requisição segue para o restante da cadeia — se o token for inválido ou ausente, o filtro simplesmente deixa a requisição prosseguir sem autenticação, e é a etapa de authorizeHttpRequests que decide se aquele endpoint exige login (retornando 401 via AuthenticationEntryPoint, como visto no capítulo anterior).
Refresh token e rotação. Tokens de acesso (access tokens) devem ter vida curta (minutos a poucas horas) para limitar o estrago de um vazamento — mas isso obrigaria o usuário a logar de novo com frequência. A solução é um segundo token, o refresh token, com vida mais longa e escopo único (trocar-se por um novo access token em um endpoint dedicado, ex. /auth/refresh). Uma prática de segurança importante é a rotação: a cada uso do refresh token, um novo é emitido e o anterior é invalidado (geralmente registrado em banco/Redis com um identificador único, o jti), permitindo detectar reuso indevido — se um refresh token já usado aparecer de novo, é sinal de que foi roubado, e toda a cadeia de tokens daquela sessão pode ser revogada.
Gotcha comum: logout stateless. Como o servidor não guarda sessão, “invalidar” um JWT antes do seu exp natural não é trivial — não existe um session.invalidate(). As soluções usuais são: manter uma blocklist de tokens revogados (por jti) em Redis com TTL igual ao tempo restante do token, ou aceitar que o token de acesso viva pouco e delegar o controle de revogação ao refresh token (que aí sim é persistido e pode ser apagado no logout).
💻 2. Código de Demonstração Corporativo
package com.empresa.api.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
@Service
public class TokenService {
private final SecretKey chave;
public TokenService(@Value("${jwt.secret}") String segredo) {
this.chave = Keys.hmacShaKeyFor(segredo.getBytes());
}
public String gerarToken(String username, List<String> papeis) {
return Jwts.builder()
.subject(username)
.claim("papeis", papeis)
.issuedAt(Date.from(Instant.now()))
.expiration(Date.from(Instant.now().plus(15, ChronoUnit.MINUTES)))
.signWith(chave)
.compact();
}
public Claims validarEExtrairClaims(String token) {
// Lança JwtException (assinatura inválida) ou ExpiredJwtException (expirado)
return Jwts.parser()
.verifyWith(chave)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
package com.empresa.api.security;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenService tokenService;
public JwtAuthenticationFilter(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
try {
var claims = tokenService.validarEExtrairClaims(header.substring(7));
List<String> papeis = claims.get("papeis", List.class);
var authorities = papeis.stream().map(SimpleGrantedAuthority::new).toList();
var authentication = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
// Token inválido/expirado: contexto permanece anônimo; authorizeHttpRequests decide o 401.
SecurityContextHolder.clearContext();
}
}
chain.doFilter(request, response);
}
}
🔗 Recursos Pedagógicos do Capítulo 10
| Recurso Didático | Finalidade | Link de Acesso |
|---|---|---|
| 📊 Slides de Aula | Apresentação visual interativa com Dark Mode e suporte a teclado | Ver Slides |
| 🧠 Quiz Formativo | Teste interativo de fixação com feedback imediato por alternativa | Fazer Quiz |
| 💻 Exemplos de Código | Demonstrações funcionais com código executável | Ver Exemplos |
| 🧩 Exercícios em 4 Níveis | Lista progressiva de fixação com gabarito em bloco colapsável | Resolver Exercícios |
| ⬅️ Capítulo Anterior | 📚 Sumário de Tópicos | Próximo Capítulo ➡️ |