Pular para o conteúdo principal

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á:

  1. ✅ Entender como funciona o JWT
  2. ✅ Decodificar a estrutura de um token
  3. ✅ Gerar e validar tokens
  4. ✅ Implementar um filtro de autenticação JWT
  5. ✅ Gerenciar refresh tokens

1. Por que JWT?​

Sessão vs JWT​

AspectoSessãoJWT
ArmazenamentoServidor (memória/BD)Cliente (localStorage/cookie)
EscalabilidadeDifícil (sticky sessions)Fácil (stateless)
MobileComplicadoPerfeito
MicroservicesProblemáticoIdeal

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​

ComponentePapel
JwtServiceGera e valida os tokens
JwtAuthenticationFilterIntercepta e processa os tokens
AuthControllerEndpoints 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


⬅️ Voltar ao Módulo 8