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


Funcionamento:

Request de usuário não autenticado será rejeitada (401 Unauthorized). Primeiramente, usuário regitra suas credenciais (/api/auth/register), depois acessa (/api/auth/login) para obter access token, para então acessar endpoints protegidos. Usuário pode gerar refresh token (/api/auth/refresh) quando o access token expirar. Por fim, usuário pode fazer logout (/api/auth/logout) com exclusão do refresh token.


Fluxo do projeto abaixo:

Cliente registra suas credenciais (/api/auth/register), acessa (/api/auth/login), recebe tokens, depois envia access token na request protegida (/api/hello). Filtro valida token, liberando acesso ao controller. Nesse exemplo simples, refresh token é gerado apenas para demonstração (ideal é gerar em /api/auth/refresh).

  1. SecurityConfig.kt define regras de segurança, libera /api/auth/** (register e login) e protege demais endpoints, além de registrar o 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. RegisterRequest.kt define formato da requisição de cadastro com username e password;
  5. LoginRequest.kt define formato da requisição de login com username e password;
  6. TokenResponse.kt define formato da resposta contendo accessToken e refreshToken;
  7. UserService.kt gerencia usuários em memória, permitindo cadastro (register) e validação de credenciais no login;
  8. AuthController.kt expõe /api/auth/register para cadastrar usuário e /api/auth/login para autenticar, validar credenciais via UserService, gerar tokens com JwtService e retornar a resposta;
  9. HelloController.kt expõe endpoint /api/hello que está protegido pelo Spring Security e exige token válido;
  10. 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. Nesse exemplo simples, refresh token é gerado apenas para demonstração (ideal é gerar em /api/auth/refresh).

  1. Pré-requisitos:
    • JDK 21 (compilador Java);
    • Kotlin (compilador Kotlin);
    • Gradle (gerenciador de dependências);
    • IntelliJ IDEA (editor de código);
    • Postman (cliente consumidor de API).
  2. 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)
  3. No projeto, criar pacotes:
    
    com.example.myjwt.controller
    com.example.myjwt.dto
    com.example.myjwt.security
    com.example.myjwt.service
    
  4. 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()
    }
    
  5. No pacote 'myjwt.dto', criar DTO 'HelloResponse.kt':
    
    package com.example.myjwt.dto
    
    data class HelloResponse(
        val message: String,
        val status: String
    )
    
  6. 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"
            )
        }
    }
    
  7. Em 'build.gradle.kts', comentar implementation de segurança:
    
    '//implementation("org.springframework.boot:spring-boot-starter-security")'
    
  8. Construir projeto: ./gradlew clean build
  9. Executar projeto: ./gradlew bootRun
  10. Em 'build.gradle.kts', remover comentário implementation de segurança:
    
    'implementation("org.springframework.boot:spring-boot-starter-security")'
    
  11. 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
        }
    }
    
  12. 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()
        }
    }
    
  13. 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)
        }
    }
    
  14. No pacote 'myjwt.dto', criar DTO 'RegisterRequest.kt':
    
    package com.example.myjwt.dto
    
    data class RegisterRequest(
        val username: String,
        val password: String
    )
    
  15. No pacote 'myjwt.dto', criar DTO 'LoginRequest.kt':
    
    package com.example.myjwt.dto
    
    data class LoginRequest(
        val username: String,
        val password: String
    )
    
  16. No pacote 'myjwt.dto', criar DTO 'TokenResponse.kt':
    
    package com.example.myjwt.dto
    
    data class TokenResponse(
        val accessToken: String,
        val refreshToken: String
    )
    
  17. No pacote 'myjwt.service', criar repositório Service 'UserService.kt':
    
    package com.example.myjwt.service
    
    import org.springframework.stereotype.Service
    
    @Service
    class UserService {
    
        private val users = mutableMapOf<String, String>()
    
        fun register(username: String, password: String) {
            if (users.containsKey(username)) {
                throw RuntimeException("User already exists")
            }
            users[username] = password
        }
    
        fun validate(username: String, password: String): Boolean {
            return users[username] == password
        }
    }
    
  18. No pacote 'myjwt.controller', criar Controller 'AuthController.kt':
    
    package com.example.myjwt.controller
    
    import com.example.myjwt.dto.LoginRequest
    import com.example.myjwt.dto.RegisterRequest
    import com.example.myjwt.dto.TokenResponse
    import com.example.myjwt.security.JwtService
    import com.example.myjwt.service.UserService
    import org.springframework.web.bind.annotation.*
    
    @RestController
    @RequestMapping("/api/auth")
    class AuthController(
        private val jwtService: JwtService,
        private val userService: UserService
    ) {
    
        @PostMapping("/register")
        fun register(@RequestBody request: RegisterRequest): String {
            userService.register(request.username, request.password)
            return "User registered successfully"
        }
    
        @PostMapping("/login")
        fun login(@RequestBody request: LoginRequest): TokenResponse {
    
            val isValid = userService.validate(request.username, request.password)
    
            if (!isValid) {
                throw RuntimeException("Invalid credentials")
            }
    
            val accessToken = jwtService.generateAccessToken(request.username)
            val refreshToken = jwtService.generateRefreshToken(request.username)
            return TokenResponse(accessToken, refreshToken)
        }
    }
    
  19. Construir projeto: ./gradlew clean build
  20. Executar projeto: ./gradlew bootRun
  21. Testar request sem autenticação: GET http://localhost:8080/api/hello
    • Response com erro (401 Unauthorized)
  22. Registrar novo usuário: POST http://localhost:8080/api/auth/register
    
    {
      "username": "admin",
      "password": "123"
    }
    
  23. Acessar e criar token JWT (copiar token gerado): POST http://localhost:8080/api/auth/login
    
    {
      "username": "admin",
      "password": "123"
    }
    
  24. 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