Autenticación JWT en Spring Boot 3 paso a paso
Cómo implementar autenticación stateless con JWT en Spring Boot 3 usando Spring Security 6, desde cero y sin librerías externas innecesarias.
JSON Web Tokens (JWT) son el estándar para autenticación stateless en APIs REST. En este tutorial implementamos el flujo completo en Spring Boot 3 con Kotlin.
¿Qué vamos a construir?
Un endpoint /auth/login que devuelve un JWT, y un filtro que valida ese token en cada request. Sin bases de datos externas para las sesiones, sin estado en el servidor.
Dependencias
En build.gradle.kts:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
}
Generación del token
@Service
class JwtService(
@Value("\${jwt.secret}") private val secret: String,
@Value("\${jwt.expiration-ms:86400000}") private val expirationMs: Long,
) {
private val key: SecretKey by lazy {
Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret))
}
fun generate(username: String): String =
Jwts.builder()
.subject(username)
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis() + expirationMs))
.signWith(key)
.compact()
fun extractUsername(token: String): String =
Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token).payload.subject
}
Filtro de autenticación
El filtro intercepta cada request, extrae el token del header Authorization: Bearer <token> y lo valida.
@Component
class JwtAuthFilter(
private val jwtService: JwtService,
private val userDetailsService: UserDetailsService,
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
) {
val header = request.getHeader("Authorization")
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response)
return
}
val token = header.removePrefix("Bearer ")
val username = runCatching { jwtService.extractUsername(token) }.getOrNull()
if (username != null && SecurityContextHolder.getContext().authentication == null) {
val userDetails = userDetailsService.loadUserByUsername(username)
val auth = UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.authorities
)
auth.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = auth
}
chain.doFilter(request, response)
}
}
Configuración de Spring Security
@Configuration
@EnableWebSecurity
class SecurityConfig(private val jwtAuthFilter: JwtAuthFilter) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
.csrf { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests {
it.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
.build()
}
Endpoint de login
@RestController
@RequestMapping("/auth")
class AuthController(
private val authManager: AuthenticationManager,
private val jwtService: JwtService,
) {
@PostMapping("/login")
fun login(@RequestBody body: LoginRequest): ResponseEntity<Map<String, String>> {
authManager.authenticate(
UsernamePasswordAuthenticationToken(body.username, body.password)
)
val token = jwtService.generate(body.username)
return ResponseEntity.ok(mapOf("token" to token))
}
}
data class LoginRequest(val username: String, val password: String)
Variables de entorno
En application.yml:
jwt:
secret: ${JWT_SECRET}
expiration-ms: 86400000
Generá el secret con openssl rand -base64 64 y ponelo en tu .env o en las variables de entorno del servidor.
Probarlo
# Login
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
# Request autenticado
curl http://localhost:8080/api/datos \
-H "Authorization: Bearer <token>"
Con esto tenés autenticación JWT completamente funcional y stateless. El siguiente paso es agregar refresh tokens para no obligar al usuario a loguearse cada 24 horas.