404 Not Found

404 Not Found


nginx

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


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:

JAVA
// 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

100%
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 .:

TEXT
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

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Saída:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Configuração JWT do SecurityFilterChain

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

TEXT
// 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

JAVA
@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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Emitindo um JWT no login

JAVA
@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:

TEXT
// 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

JAVA
@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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Usando Informações JWT em um Controller

JAVA
@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:

TEXT
// 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

JAVA
// 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) {}
}
💻 Processo de Teste:

BASH
# 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

P Qual é a principal diferença entre JWT e sessões?
R Sessões são stateful (armazenadas no servidor), enquanto JWTs são stateless (tokens autocontidos). JWTs são adequados para microsserviços e escalabilidade horizontal, enquanto sessões são adequadas para aplicações monolíticas. JWTs não podem ser revogados ativamente (a menos que uma lista negra seja implementada), enquanto sessões podem ser destruídas imediatamente.
P JWT usa criptografia simétrica ou assimétrica?
R Criptografia assimétrica (RSA/ECDSA) é recomendada para ambientes de produção. O Resource Server só precisa verificar a chave pública; a chave privada é mantida apenas pelo Authorization Server. Criptografia simétrica (HMAC) carrega alto risco de comprometimento de chave.
P O que deve ser feito quando um JWT expira?
R Abordagens comuns: 1) O mecanismo de refresh token, que usa um token de acesso de curto prazo e um refresh token de longo prazo; 2) O cliente detecta a expiração e redireciona automaticamente para a página de login; 3) Definir um tempo de expiração razoável (1-4 horas).
P Como revogar um JWT emitido?
R Um JWT não pode ser revogado por si só. Abordagens comuns incluem: 1) tempo de expiração curto + refresh token; 2) lista negra de tokens (armazenada no Redis); 3) alterar a chave de assinatura para invalidar todos os tokens antigos.
P Qual é a relação entre o OAuth 2.0 Resource Server e o Authorization Server?
R O Authorization Server é responsável por emitir tokens, enquanto o Resource Server é responsável por verificar tokens para proteger recursos. Nesta aula, o OrderFlow atua como ambos—Resource Server e uma implementação simplificada do Authorization Server. Para ambientes de produção, recomendamos usar soluções profissionais como Keycloak.
P Quanta informação deve ser incluída nas claims JWT?
R Siga o princípio do minimalismo. Inclua apenas o identificador do usuário (sub) e papel; não inclua informações sensíveis (como senhas ou números de telefone). JWTs são codificados em Base64, então qualquer pessoa pode decodificá-los; a integridade é garantida apenas pela assinatura.

📖 Resumo


📝 Exercícios

  1. 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%