Capítulo 3: JWT (JSON Web Tokens)
Introdução
Sessions são ótimas para aplicações web tradicionais, mas as APIs REST precisam ser stateless (sem estado). JWT (JSON Web Token) é a solução padrão para autenticar requisições de API sem armazenar sessão no lado do servidor.
🎯 Objetivos do Capítulo
Ao final deste capítulo, você saberá:
- ✅ Entender como funciona o JWT
- ✅ Decodificar a estrutura de um token
- ✅ Gerar e validar tokens
- ✅ Implementar um filtro de autenticação JWT
- ✅ Gerenciar refresh tokens
1. Por que JWT?
Sessão vs JWT
| Aspecto | Sessão | JWT |
|---|---|---|
| Armazenamento | Servidor (memória/BD) | Cliente (localStorage/cookie) |
| Escalabilidade | Difícil (sticky sessions) | Fácil (stateless) |
| Mobile | Complicado | Perfeito |
| Microservices | Problemático | Ideal |
Fluxo JWT
2. Estrutura de um JWT
As 3 Partes
Um JWT é composto por 3 partes separadas por pontos:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
└──────────── HEADER ──────────────────┴────────────────────── PAYLOAD ────────────────────────────────┴── SIGNATURE ──
Header (Cabeçalho)
{
"alg": "HS256", // Algoritmo de assinatura
"typ": "JWT" // Tipo do token
}
Codificado em Base64: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload (Carga Útil)
{
"sub": "1234567890", // Subject (ID do usuário)
"name": "John Doe", // Claims customizadas
"email": "john@example.com",
"roles": ["ROLE_USER"],
"iat": 1516239022, // Issued At (timestamp)
"exp": 1516242622 // Expiração (timestamp)
}
Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
atenção
O payload é codificado, não criptografado! Nunca coloque dados sensíveis (senha, cartão de crédito...) nele.
3. Implementação com Spring Boot
3.1 Dependências
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
3.2 Configuração
# application.properties
jwt.secret=suaChaveSecretaBemLongaEComplexaComPeloMenos256Bits
jwt.expiration=86400000
jwt.refresh-expiration=604800000
@Configuration
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {
private String secret;
private long expiration; // 24h em ms
private long refreshExpiration; // 7 dias em ms
}
3.3 JwtService
@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtProperties jwtProperties;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecret());
return Keys.hmacShaKeyFor(keyBytes);
}
// Gerar um Access Token
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
.signWith(getSigningKey())
.compact();
}
// Gerar um Refresh Token
public String generateRefreshToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration()))
.signWith(getSigningKey())
.compact();
}
// Extrair o username
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extrair uma claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// Validar o token
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
3.4 JwtAuthenticationFilter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Recuperar o header Authorization
final String authHeader = request.getHeader("Authorization");
// Verificar o formato Bearer
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Extrair o token
final String jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);
// Verificar se ainda não está autenticado
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// Carregar o usuário
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// Validar o token
if (jwtService.isTokenValid(jwt, userDetails)) {
// Criar a autenticação
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Armazenar no SecurityContext
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
3.5 SecurityConfig para JWT
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Desabilitar CSRF para API REST
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
4. Controller de Autenticação
DTOs
// Requisição de login
@Data
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
}
// Requisição de cadastro
@Data
public class RegisterRequest {
@NotBlank
@Size(min = 3, max = 50)
private String username;
@NotBlank
@Email
private String email;
@NotBlank
@Size(min = 6)
private String password;
}
// Resposta com tokens
@Data
@Builder
public class AuthResponse {
private String accessToken;
private String refreshToken;
private String username;
private List<String> roles;
}
// Requisição de refresh
@Data
public class RefreshTokenRequest {
@NotBlank
private String refreshToken;
}
AuthController
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtService jwtService;
private final UserService userService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
// Autenticar
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// Carregar o usuário
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
// Gerar os tokens
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(userDetails.getUsername())
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build());
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
// Criar o usuário
User user = userService.registerUser(
request.getUsername(),
request.getEmail(),
request.getPassword()
);
// Carregar UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
// Gerar os tokens
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.status(HttpStatus.CREATED)
.body(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(user.getUsername())
.roles(List.of("ROLE_USER"))
.build());
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
String username = jwtService.extractUsername(request.getRefreshToken());
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(request.getRefreshToken(), userDetails)) {
String accessToken = jwtService.generateToken(userDetails);
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(request.getRefreshToken()) // Reutilizar
.username(username)
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build());
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
5. Teste com cURL
Login
# Login
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"password"}'
# Resposta:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"username": "user",
"roles": ["ROLE_USER"]
}
Acesso Protegido
# Sem token
curl http://localhost:8080/api/users
# 401 Unauthorized
# Com token
curl http://localhost:8080/api/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 200 OK
Refresh Token
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"eyJhbGciOiJIUzI1NiJ9..."}'
6. Boas Práticas
Armazenamento do Token (Frontend)
// ✅ httpOnly Cookie (mais seguro)
// Gerenciado pelo servidor
// ⚠️ localStorage (vulnerável a XSS)
localStorage.setItem('token', response.accessToken);
// ⚠️ sessionStorage (melhor que localStorage)
sessionStorage.setItem('token', response.accessToken);
Tempo de Vida
Access Token: 15 minutos - 1 hora (curto)
Refresh Token: 7 dias - 30 dias (longo)
Revogação
// Opção 1: Blacklist no BD
@Entity
public class RevokedToken {
private String tokenId;
private LocalDateTime revokedAt;
}
// Opção 2: Versão do usuário
// Alterar user.tokenVersion invalida todos os tokens antigos
7. Resumo
Estrutura JWT
HEADER.PAYLOAD.SIGNATURE
Componentes Chave
| Componente | Papel |
|---|---|
| JwtService | Gera e valida os tokens |
| JwtAuthenticationFilter | Intercepta e processa os tokens |
| AuthController | Endpoints login/register/refresh |
Fluxo Completo
1. POST /api/auth/login → Access + Refresh tokens
2. GET /api/data (Bearer token) → Dados
3. Token expirado → POST /api/auth/refresh → Novo Access token
🎯 Próximo Passo
JWT é perfeito para autenticação direta, mas às vezes queremos delegar a autenticação para um terceiro (Google, GitHub...). Esse é o papel do OAuth2.
➡️ Capítulo 4: OAuth2 e Login Social