OAuth2 e Autenticação JWT
JWT é o passaporte dos microsserviços—stateless, autocontido e habilitando autenticação entre serviços—pondo fim aos desafios do compartilhamento de sessão.
1. O Que Você Vai Aprender
- Comparação do Fluxo de Código de Autorização OAuth 2.0 e a Concessão por Senha
- Estrutura do Token JWT: Header / Payload / Signature
- Configuração do
spring-boot-starter-oauth2-resource-servere Decodificação JWT - Mapear Claims JWT Personalizados para Papéis de Usuário
- Alice implementa emissão de JWT e autenticação por token de API para o OrderFlow
2. Uma História Real de um Arquiteto
(1) Ponto de Dor: Sessões Expiram em Microsserviços
Depois que Alice escalou o OrderFlow de uma única instância para três instâncias, surgiu um problema de sessão: quando um usuário fazia login na Instância A e a requisição era balanceada para a Instância B, a sessão não existia, e o usuário era desconectado. Bob tentou usar Redis para compartilhar sessões, mas isso introduziu novas dependências e complexidade. Charlie solicitou suporte para login de terceiros (Google/GitHub), o que a solução tradicional de sessão era completamente incapaz de acomodar.
(2) Solução JWT
Tokens JWT contêm toda informação de autenticação, então o servidor não precisa armazenar sessões:
// Emitir JWT no login
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
// Verificar JWT em cada requisição (stateless, sem sessão necessária)
// Spring Security extrai automaticamente papéis das claims JWT
(3) Resultado
Depois que Alice substituiu sessões por JWTs: as três instâncias não precisam mais compartilhar sessão, habilitando escalabilidade horizontal com custo zero; tokens são armazenados no lado do cliente, tornando o servidor stateless; e login OAuth 2.0 de terceiros é suportado.
3. Conceitos de OAuth 2.0 e JWT
(1) Fluxo de Autorização OAuth 2.0
sequenceDiagram
participant U as User Agent<br/>(Browser)
participant C as Client<br/>(OrderFlow SPA)
participant AS as Authorization<br/>Server
participant RS as Resource<br/>Server (API)
U->>C: Click "Login with GitHub"
C->>AS: Redirect to /authorize
AS->>U: Login + Consent page
U->>AS: Approve
AS->>C: Redirect back with auth code
C->>AS: POST /token (auth code + client_secret)
AS->>C: Access Token (JWT)
C->>RS: GET /api/orders (Bearer token)
RS->>RS: Verify JWT signature
RS->>C: 200 OK + Data
| Modelo de Autorização | Casos de Uso | Tipos de Cliente |
|---|---|---|
| Authorization Code Flow | Aplicações Web / SPAs | Serviços Backend + Frontend |
| Client Credentials | Chamadas entre serviços | Machine-to-Machine |
| Resource Owner Password Credentials (ROPC) | Descontinuado, apenas para teste | Aplicações próprias |
| Implicit Mode | Descontinuado | SPA legado |
(2) Estrutura do Token JWT
Um JWT consiste em três partes, separadas por .:
Header.Payload.Signature
eyJhbGciOiJS256.eyJzdWIiOiJib2IiLCJyb2xlIjoiQ1VTVU9NRVIifQ.signature
| Seção | Conteúdo | Exemplo |
|---|---|---|
| Header | Algoritmo + Tipo do Token | {"alg":"RS256","typ":"JWT"} |
| Payload | Claims | {"sub":"bob","role":"CUSTOMER","exp":1705312000} |
| Signature | Assinatura do Header+Payload | Verificação de Integridade da Assinatura RS256 |
| Claim Padrão | Significado |
|---|---|
sub |
Assunto (ID do Usuário) |
iss |
Emissor |
aud |
Audiência |
exp |
Expiração |
iat |
Emitido Em (Data de Emissão) |
scope |
Escopo de Permissões |
4. Configuração do OAuth 2.0 Resource Server
(1) Dependências e Configuração
(1) ▶ Exemplo: Dependências pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Configuração JWT do SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
String role = jwt.getClaimAsString("role");
if (role == null) return List.of();
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
});
return converter;
}
}
Saída:
// Execução bem-sucedida
5. Emissão e Verificação de JWT
(1) Configuração do Par de Chaves RSA
(1) ▶ Exemplo: Gerando um par de chaves RSA
@Configuration
public class JwtConfig {
@Bean
public KeyPair keyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
}
@Bean
public JwtEncoder jwtEncoder(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSource<SecurityContext> jwkSource =
new ImmutableJWKSet<>(new JWKSet(rsaKey));
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return JwtDecoders.withPublicKey((RSAPublicKey) keyPair.getPublic());
}
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Emitindo um JWT no login
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authenticationManager;
public AuthController(JwtEncoder jwtEncoder,
AuthenticationManager authenticationManager) {
this.jwtEncoder = jwtEncoder;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
Instant now = Instant.now();
Instant expiry = now.plus(1, ChronoUnit.HOURS);
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow-service")
.subject(auth.getName())
.issuedAt(now)
.expiresAt(expiry)
.claim("role", auth.getAuthorities().stream()
.filter(a -> a.getAuthority().startsWith("ROLE_"))
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder
.encode(JwtEncoderParameters.from(claims))
.getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
Saída:
// Execução bem-sucedida
6. Claims JWT Personalizados e Mapeamentos de Papel
(1) Estratégia de Mapeamento de Papel
| Campo Claim | Valor | Mapeado para Authority |
|---|---|---|
role: "ADMIN" |
String de papel único | ROLE_ADMIN |
roles: ["ADMIN","CUSTOMER"] |
Array de papéis | ROLE_ADMIN, ROLE_CUSTOMER |
scope: "read write" |
Escopo OAuth2 | SCOPE_read, SCOPE_write |
(1) ▶ Exemplo: JwtAuthenticationConverter Personalizado
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
// Suporta tanto claims "role" (único) quanto "roles" (array)
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
String role = jwt.getClaimAsString("role");
if (role != null) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
return authorities;
});
return converter;
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Usando Informações JWT em um Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/my")
public List<OrderResponse> getMyOrders(
@AuthenticationPrincipal Jwt jwt) {
String username = jwt.getClaimAsString("sub");
String role = jwt.getClaimAsString("role");
return orderService.getOrdersByUsername(username);
}
}
Saída:
// Execução bem-sucedida
| Método | Obtenção de Informações do Usuário | Cenários Aplicáveis |
|---|---|---|
@AuthenticationPrincipal Jwt jwt |
Claims JWT completas | Claim personalizado necessário |
@AuthenticationPrincipal UserDetails user |
UserDetails | Informações padrão de usuário necessárias |
Principal principal |
getName() | Apenas nome de usuário |
7. Exemplo Abrangente: Implementação Completa da Autenticação JWT do OrderFlow
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt ->
Optional.ofNullable(jwt.getClaimAsString("role"))
.map(role -> List.of(new SimpleGrantedAuthority("ROLE_" + role)))
.orElse(List.of()));
return converter;
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService, PasswordEncoder encoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}
}
// AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authManager;
public AuthController(JwtEncoder jwtEncoder, AuthenticationManager authManager) {
this.jwtEncoder = jwtEncoder;
this.authManager = authManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest req) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password()));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow").subject(auth.getName())
.issuedAt(now).expiresAt(now.plus(1, ChronoUnit.HOURS))
.claim("role", auth.getAuthorities().stream()
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
# 1. Login e obter JWT
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"pass123"}' | jq -r '.accessToken')
# 2. Acessar API protegida com JWT
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/orders/my
# 3. Endpoint admin com JWT de admin
ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"admin123"}' | jq -r '.accessToken')
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:8080/api/v1/admin/dashboard
❓ Perguntas Frequentes
📖 Resumo
- As três partes de um JWT: Header (algoritmo), Payload (claims) e Signature (assinatura)
- O OAuth2 Resource Server verifica a assinatura JWT e extrai papéis das claims para conceder autorização
- Criptografia assimétrica RSA: emissão com chave privada, verificação com chave pública, e gerenciamento de chaves mais seguro
JwtAuthenticationConverterLógica de mapeamento personalizada de Claims para Authority- A API de login emite um JWT; outras APIs usam autenticação Bearer Token
- JWT é stateless e bem adequado para microsserviços; sua desvantagem é que não pode ser revogado ativamente
📝 Exercícios
-
Exercício Básico (Dificuldade ⭐): Implemente login JWT e autenticação de API para o OrderFlow, substituindo httpBasic. Use curl para testar o processo de login e obter um token → Use o token para acessar a API protegida.
-
Exercício Avançado (Dificuldade: ⭐⭐): Implemente o mecanismo de Refresh Token—o Access Token é válido por 15 minutos, o Refresh Token é válido por 7 dias, e o Refresh Token é usado para obter um novo Access Token.
-
Desafio (Dificuldade: ⭐⭐⭐): Integre Keycloak como um servidor de autorização externo, com OrderFlow atuando apenas como um resource server para validar JWTs emitidos pelo Keycloak. Considere como estabelecer relacionamentos de confiança entre os serviços.



