JWT

Implementação de JSON Web Token
Voltar

Material complementar:


Conceito:

JWT (JSON Web Token) é padrão (RFC 7519) de token stateless (geralmente do tipo Bearer token) usado para autenticação/autorização na troca de informações entre cliente e servidor (HTTP request/response) de forma segura (geralmente em conjunto com protocolo OAuth2), assinadas com Message Authentication Code (HMAC). Access Token é token de curta duração com expiração, usado para acessar recursos protegidos da API, enviado em cada request no header Authorization. Refresh Token é token de longa duração de expiração, usado apenas para gerar novos access tokens. Sua estrutura consiste em header (tipo de token e algoritmo de assinatura), payload (dados claims, como subject, issuedAt, expiration) e signature (assinatura criptográfica do header e payload usando secret key). JWT amplamente utilizado em sistemas web RESTful (arquitetura REST) stateless (sem manter estado no servidor), com funções encoder (criação) e decoder (leitura).


Fluxo do projeto abaixo:

Cliente acessa /api/auth/login, recebe tokens, depois envia access token em /api/hello. Filtro valida token, liberando acesso ao controller.

  1. SecurityConfig.kt define regras de segurança, libera /api/auth/** e protege demais endpoints, além de registrar filtro JWT;
  2. JwtAuthenticationFilter.kt intercepta todas requisições, lê header Authorization, valida token e define usuário no SecurityContext;
  3. JwtService.kt gera access e refresh token, e extrai username do token;
  4. LoginRequest.kt define formato da requisição de login com username e password;
  5. TokenResponse.kt define formato da resposta contendo accessToken e refreshToken;
  6. AuthController.kt recebe requisição /api/auth/login, valida credenciais (mock), invoca JwtService e retorna tokens;
  7. HelloController.kt expõe endpoint /api/hello que está protegido pelo Spring Security e exige token válido;
  8. HelloResponse.kt define formato de resposta do endpoint protegido ou público de teste.

Implementação:

Projeto Spring Boot Kotlin API REST para implementação de JWT.

  1. Criar projeto em https://start.spring.io
    • Project: Gradle - Kotlin
    • Language: Kotlin
    • Spring Boot: 4.0.6
    • Project Metadata:
      • Group: com.example
      • Artifact: myjwt
      • Package name: com.example.myjwt
      • Packaging: Jar
      • Configuration: properties
      • Java: 21
    • Generate
      • Extrair e abrir projeto no editor (IntelliJ IDEA)
  2. No projeto, criar pacotes:
    
    com.example.myjwt.controller
    com.example.myjwt.dto
    com.example.myjwt.security
    
  3. Conteúdo de 'build.gradle.kts':
    
    plugins {
    	kotlin("jvm") version "2.2.21"
    	kotlin("plugin.spring") version "2.2.21"
    	id("org.springframework.boot") version "4.0.6"
    	id("io.spring.dependency-management") version "1.1.7"
    }
    
    group = "com.example"
    version = "0.0.1-SNAPSHOT"
    
    java {
    	toolchain {
    		languageVersion = JavaLanguageVersion.of(21)
    	}
    }
    
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	implementation("org.springframework.boot:spring-boot-starter-web")
    	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    	implementation("org.jetbrains.kotlin:kotlin-reflect")
    	implementation("org.springframework.boot:spring-boot-starter-security")
    	developmentOnly("org.springframework.boot:spring-boot-devtools")
    	testImplementation("org.springframework.boot:spring-boot-starter-test")
    	implementation("io.jsonwebtoken:jjwt-api:0.12.6")
    	runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
    	runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
    }
    
    kotlin {
    	compilerOptions {
    		freeCompilerArgs.addAll("-Xjsr305=strict")
    	}
    }
    
    tasks.withType<Test> {
    	useJUnitPlatform()
    }
    
  4. No pacote 'myjwt.dto', criar DTO 'HelloResponse.kt':
    
    package com.example.myjwt.dto
    
    data class HelloResponse(
        val message: String,
        val status: String
    )
    
  5. No pacote 'myjwt.controller', criar Controller 'HelloController.kt':
    
    package com.example.myjwt.controller
    
    import com.example.myjwt.dto.HelloResponse
    import org.springframework.web.bind.annotation.GetMapping
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RestController
    
    @RestController
    @RequestMapping("/api")
    class HelloController {
    
        @GetMapping("/hello")
        fun hello(): HelloResponse {
            return HelloResponse(
                message = "API REST funcionando!",
                status = "OK"
            )
        }
    }
    
  6. Em 'build.gradle.kts', comentar implementation de segurança:
    
    '//implementation("org.springframework.boot:spring-boot-starter-security")'
    
  7. Construir projeto: ./gradlew clean build
  8. Executar projeto: ./gradlew bootRun
  9. Em 'build.gradle.kts', remover comentário implementation de segurança:
    
    'implementation("org.springframework.boot:spring-boot-starter-security")'
    
  10. No pacote 'myjwt.security', criar Service 'JwtService.kt':
    
    package com.example.myjwt.security
    
    import io.jsonwebtoken.Jwts
    import io.jsonwebtoken.security.Keys
    import org.springframework.stereotype.Service
    import java.util.*
    import javax.crypto.SecretKey
    
    @Service
    class JwtService {
    
        private val secretKey: SecretKey = Keys.hmacShaKeyFor(
            "my-super-secret-key-my-super-secret-key".toByteArray()
        )
    
        private val accessExpirationMs = 1000 * 60 * 5L
        private val refreshExpirationMs = 1000 * 60 * 60 * 24L
    
        fun generateAccessToken(username: String): String {
            return Jwts.builder()
                .subject(username)
                .issuedAt(Date())
                .expiration(Date(System.currentTimeMillis() + accessExpirationMs))
                .signWith(secretKey)
                .compact()
        }
    
        fun generateRefreshToken(username: String): String {
            return Jwts.builder()
                .subject(username)
                .issuedAt(Date())
                .expiration(Date(System.currentTimeMillis() + refreshExpirationMs))
                .claim("type", "refresh")
                .signWith(secretKey)
                .compact()
        }
    
        fun extractUsername(token: String): String {
            return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .payload
                .subject
        }
    }
    
  11. No pacote 'myjwt.security', criar classe 'SecurityConfig.kt':
    
    package com.example.myjwt.security
    
    import jakarta.servlet.http.HttpServletResponse
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import org.springframework.security.config.annotation.web.builders.HttpSecurity
    import org.springframework.security.config.http.SessionCreationPolicy
    import org.springframework.security.web.SecurityFilterChain
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    
    @Configuration
    class SecurityConfig(
        private val jwtAuthenticationFilter: JwtAuthenticationFilter
    ) {
    
        @Bean
        fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
    
            http
                .csrf { it.disable() }
                .formLogin { it.disable() }
                .httpBasic { it.disable() }
                
                .exceptionHandling {
                    it.authenticationEntryPoint { _, response, _ ->
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
                    }
                }
    
                .sessionManagement {
                    it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                }
    
                .authorizeHttpRequests {
                    it
                        .requestMatchers("/api/auth/**").permitAll()
                        .anyRequest().authenticated()
                }
    
                .addFilterBefore(
                    jwtAuthenticationFilter,
                    UsernamePasswordAuthenticationFilter::class.java
                )
    
            return http.build()
        }
    }
    
  12. No pacote 'myjwt.security', criar classe de filtro 'JwtAuthenticationFilter.kt':
    
    package com.example.myjwt.security
    import jakarta.servlet.FilterChain
    import jakarta.servlet.http.HttpServletRequest
    import jakarta.servlet.http.HttpServletResponse
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
    import org.springframework.security.core.context.SecurityContextHolder
    import org.springframework.security.core.userdetails.User
    import org.springframework.stereotype.Component
    import org.springframework.web.filter.OncePerRequestFilter
    
    @Component
    class JwtAuthenticationFilter(
        private val jwtService: JwtService
    ) : OncePerRequestFilter() {
    
        override fun doFilterInternal(
            request: HttpServletRequest,
            response: HttpServletResponse,
            filterChain: FilterChain
        ) {
            val authHeader = request.getHeader("Authorization")
            
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                filterChain.doFilter(request, response)
                return
            }
    
            val token = authHeader.substring(7)
    
            try {
                val username = jwtService.extractUsername(token)
                
                val auth = UsernamePasswordAuthenticationToken(
                    User(username, "", emptyList()),
                    null,
                    emptyList()
                )
    
                SecurityContextHolder.getContext().authentication = auth
    
            } catch (e: Exception) {
                SecurityContextHolder.clearContext()
            }
    
            filterChain.doFilter(request, response)
        }
    }
    
  13. No pacote 'myjwt.dto', criar DTO 'LoginRequest.kt':
    
    package com.example.myjwt.dto
    
    data class LoginRequest(
        val username: String,
        val password: String
    )
    
  14. No pacote 'myjwt.dto', criar DTO 'TokenResponse.kt':
    
    package com.example.myjwt.dto
    
    data class TokenResponse(
        val accessToken: String,
        val refreshToken: String
    )
    
  15. No pacote 'myjwt.controller', criar Controller 'AuthController.kt':
    
    package com.example.myjwt.controller
    
    import com.example.myjwt.dto.LoginRequest
    import com.example.myjwt.dto.TokenResponse
    import com.example.myjwt.security.JwtService
    import org.springframework.web.bind.annotation.*
    
    @RestController
    @RequestMapping("/api/auth")
    class AuthController(
        private val jwtService: JwtService
    ) {
    
        @PostMapping("/login")
        fun login(@RequestBody request: LoginRequest): TokenResponse {
            
            if (request.username != "admin" || request.password != "123") {
                throw RuntimeException("Invalid credentials")
            }
            
            val accessToken = jwtService.generateAccessToken(request.username)
            val refreshToken = jwtService.generateRefreshToken(request.username)
            
            return TokenResponse(
                accessToken = accessToken,
                refreshToken = refreshToken
            )
        }
    }
    
  16. Construir projeto: ./gradlew clean build
  17. Executar projeto: ./gradlew bootRun
  18. Testar request sem autenticação: GET http://localhost:8080/api/hello
    • Response com erro (401 Unauthorized)
  19. Criar token JWT (copiar token gerado): POST http://localhost:8080/api/auth/login
    
    {
      "username": "admin",
      "password": "123"
    }
    
  20. Testar request com autenticação: GET http://localhost:8080/api/hello (Authorization: Bearer token)
    • Response com sucesso (200 OK)

Elaborado por Mateus Schwede
ubsocial.github.io