Curso JWT en Spring Boot 3 #1: Fundamentos y Configuración Inicial


Bienvenido al Curso — Parte 1 de 4
Este es el primer artículo de una serie de cuatro partes donde construiremos un sistema de autenticación completo con JWT (JSON Web Tokens) en Spring Boot 3. Al finalizar el curso tendrás una API REST segura, lista para producción, con las mejores prácticas de Spring Security.
¿Qué aprenderás en todo el curso?
Parte 1 (este artículo): Fundamentos de JWT, configuración del proyecto, servicio JWT, filtro de autenticación y endpoints básicos.
Parte 2: Refresh tokens — rotación segura, almacenamiento en base de datos y flujo de renovación automática.
Parte 3: Roles y permisos — autorización basada en roles (
ROLE_ADMIN,ROLE_USER), anotaciones@PreAuthorizey control de acceso granular.Parte 4: Seguridad avanzada — blacklisting de tokens con Redis, protección contra ataques comunes (CSRF, XSS, replay) y despliegue seguro.
Prerrequisitos
Java 17+ instalado (recomendado Java 21 LTS)
Spring Boot 3.2+ (usaremos 3.4.x en los ejemplos)
Maven como gestor de dependencias
Conocimientos básicos de Spring Boot y Spring MVC
Un IDE como IntelliJ IDEA, VS Code con extensiones Java, o Eclipse
Código fuente: Todo el código de este curso está disponible y es completamente funcional. Cada clase que mostramos es compilable y puedes seguir el tutorial paso a paso para construir tu propio proyecto desde cero.
¿Qué es JWT?
Un JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define un formato compacto y autocontenido para transmitir información de forma segura entre dos partes como un objeto JSON. Esta información puede ser verificada y confiable porque está firmada digitalmente.
Los JWT se pueden firmar usando un secreto (con el algoritmo HMAC) o un par de claves pública/privada usando RSA o ECDSA. En este curso usaremos HMAC-SHA256 por su simplicidad, aunque en entornos con microservicios se recomienda RSA.
¿Por qué usar JWT para autenticación?
Stateless: El servidor no necesita almacenar sesiones. Toda la información del usuario viaja dentro del token.
Escalable: Al no depender de estado en el servidor, puedes escalar horizontalmente sin compartir sesiones entre instancias.
Cross-domain: Los JWT funcionan perfectamente en arquitecturas con múltiples dominios, microservicios y aplicaciones móviles.
Autocontenido: El token contiene toda la información necesaria sobre el usuario, eliminando la necesidad de consultar la base de datos en cada petición.
Importante: Un JWT no está cifrado por defecto — está firmado. Cualquiera puede decodificar el payload en jwt.io. Nunca incluyas información sensible (contraseñas, números de tarjeta) en el payload de un JWT.
Estructura de un JWT
Un JWT consta de tres partes separadas por puntos (.):
1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGVtYWlsLmNvbSIsImlhdCI6MTcxMjAwMDAwMCwiZXhwIjoxNzEyMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Cada parte tiene un propósito específico:
1. Header (Encabezado)
Contiene el tipo de token y el algoritmo de firma. Se codifica en Base64Url:
1{
2 "alg": "HS256",
3 "typ": "JWT"
4}
2. Payload (Carga útil)
Contiene los claims — las declaraciones sobre el usuario y metadatos adicionales. Existen tres tipos de claims:
Registered claims: Predefinidos por el estándar —
sub(subject),iat(issued at),exp(expiration),iss(issuer).Public claims: Definidos libremente, pero deben registrarse en el registro IANA para evitar colisiones.
Private claims: Claims personalizados acordados entre las partes — por ejemplo,
roles,userId.
1{
2 "sub": "[email protected]",
3 "iat": 1712000000,
4 "exp": 1712003600,
5 "roles": ["ROLE_USER"]
6}
3. Signature (Firma)
Se genera tomando el header y payload codificados, y firmándolos con el algoritmo especificado y una clave secreta:
1HMACSHA256(
2 base64UrlEncode(header) + "." + base64UrlEncode(payload),
3 secret
4)
La firma garantiza que el token no ha sido alterado. Si alguien modifica el payload, la firma no coincidirá y el servidor rechazará el token.
JWT vs Sesiones Tradicionales
Antes de JWT, la autenticación web se manejaba con sesiones del lado del servidor. Ambos enfoques tienen ventajas y desventajas que debes conocer para elegir el adecuado:
Característica | Sesiones (Stateful) | JWT (Stateless) |
|---|---|---|
Almacenamiento | Servidor (memoria/Redis) | Cliente (localStorage/cookie) |
Escalabilidad | Requiere sesiones compartidas (sticky sessions o Redis) | Escala horizontalmente sin configuración adicional |
Revocación | Inmediata — eliminas la sesión del servidor | Compleja — el token es válido hasta que expire (necesitas blacklist) |
Rendimiento | Consulta al almacén de sesiones en cada request | Validación criptográfica local, sin I/O |
Cross-domain | Difícil — cookies están limitadas al dominio | Fácil — se envía en el header Authorization |
Tamaño | ID de sesión pequeño (~32 bytes) | Token más grande (~800+ bytes con claims) |
Microservicios | Necesita servicio centralizado de sesiones | Cada servicio valida el token independientemente |
¿Cuándo usar cada uno? Usa sesiones cuando necesites revocación inmediata y tengas un monolito. Usa JWT cuando tengas microservicios, APIs públicas, o aplicaciones móviles como consumidores.
Configuración del Proyecto
Vamos a crear el proyecto desde cero usando Spring Initializr. Necesitamos las siguientes dependencias:
Spring Web — para crear endpoints REST
Spring Security — para el framework de seguridad
Spring Data JPA — para la capa de persistencia
H2 Database — base de datos en memoria para desarrollo
JJWT — la librería para generar y validar JWT (versión 0.12.x)
Lombok — para reducir boilerplate (opcional)
Agrega las siguientes dependencias a tu pom.xml:
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5 https://maven.apache.org/xsd/maven-4.0.0.xsd">
6 <modelVersion>4.0.0</modelVersion>
7
8 <parent>
9 <groupId>org.springframework.boot</groupId>
10 <artifactId>spring-boot-starter-parent</artifactId>
11 <version>3.4.1</version>
12 <relativePath/>
13 </parent>
14
15 <groupId>com.example</groupId>
16 <artifactId>jwt-security-demo</artifactId>
17 <version>1.0.0</version>
18 <name>jwt-security-demo</name>
19 <description>JWT Authentication with Spring Boot 3</description>
20
21 <properties>
22 <java.version>17</java.version>
23 <jjwt.version>0.12.6</jjwt.version>
24 </properties>
25
26 <dependencies>
27 <!-- Spring Boot Starters -->
28 <dependency>
29 <groupId>org.springframework.boot</groupId>
30 <artifactId>spring-boot-starter-web</artifactId>
31 </dependency>
32 <dependency>
33 <groupId>org.springframework.boot</groupId>
34 <artifactId>spring-boot-starter-security</artifactId>
35 </dependency>
36 <dependency>
37 <groupId>org.springframework.boot</groupId>
38 <artifactId>spring-boot-starter-data-jpa</artifactId>
39 </dependency>
40 <dependency>
41 <groupId>org.springframework.boot</groupId>
42 <artifactId>spring-boot-starter-validation</artifactId>
43 </dependency>
44
45 <!-- H2 Database -->
46 <dependency>
47 <groupId>com.h2database</groupId>
48 <artifactId>h2</artifactId>
49 <scope>runtime</scope>
50 </dependency>
51
52 <!-- JJWT (JSON Web Token) -->
53 <dependency>
54 <groupId>io.jsonwebtoken</groupId>
55 <artifactId>jjwt-api</artifactId>
56 <version>${jjwt.version}</version>
57 </dependency>
58 <dependency>
59 <groupId>io.jsonwebtoken</groupId>
60 <artifactId>jjwt-impl</artifactId>
61 <version>${jjwt.version}</version>
62 <scope>runtime</scope>
63 </dependency>
64 <dependency>
65 <groupId>io.jsonwebtoken</groupId>
66 <artifactId>jjwt-jackson</artifactId>
67 <version>${jjwt.version}</version>
68 <scope>runtime</scope>
69 </dependency>
70
71 <!-- Lombok (opcional) -->
72 <dependency>
73 <groupId>org.projectlombok</groupId>
74 <artifactId>lombok</artifactId>
75 <optional>true</optional>
76 </dependency>
77
78 <!-- Test -->
79 <dependency>
80 <groupId>org.springframework.boot</groupId>
81 <artifactId>spring-boot-starter-test</artifactId>
82 <scope>test</scope>
83 </dependency>
84 <dependency>
85 <groupId>org.springframework.security</groupId>
86 <artifactId>spring-security-test</artifactId>
87 <scope>test</scope>
88 </dependency>
89 </dependencies>
90
91 <build>
92 <plugins>
93 <plugin>
94 <groupId>org.springframework.boot</groupId>
95 <artifactId>spring-boot-maven-plugin</artifactId>
96 <configuration>
97 <excludes>
98 <exclude>
99 <groupId>org.projectlombok</groupId>
100 <artifactId>lombok</artifactId>
101 </exclude>
102 </excludes>
103 </configuration>
104 </plugin>
105 </plugins>
106 </build>
107</project>
Agrega la configuración de JWT en tu application.yml:
1spring:
2 datasource:
3 url: jdbc:h2:mem:jwtdb
4 driver-class-name: org.h2.Driver
5 username: sa
6 password:
7 jpa:
8 hibernate:
9 ddl-auto: create-drop
10 show-sql: true
11 h2:
12 console:
13 enabled: true
14
15jwt:
16 secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
17 expiration: 3600000 # 1 hora en milisegundos
¡No hardcodees el secreto! En este tutorial usamos un valor fijo para simplificar, pero en producción debes usar una variable de entorno o un servicio de gestión de secretos (AWS Secrets Manager, HashiCorp Vault). El secreto debe tener al menos 256 bits (32 bytes) para HMAC-SHA256.
Creando el Servicio JWT
El JwtService es el corazón de nuestra autenticación. Se encarga de generar tokens, validarlos y extraer la información del usuario. Usaremos la API de JJWT 0.12.x, que introdujo cambios significativos respecto a versiones anteriores.
1package com.example.jwtsecuritydemo.service;
2
3import io.jsonwebtoken.Claims;
4import io.jsonwebtoken.Jwts;
5import io.jsonwebtoken.io.Decoders;
6import io.jsonwebtoken.security.Keys;
7import org.springframework.beans.factory.annotation.Value;
8import org.springframework.security.core.userdetails.UserDetails;
9import org.springframework.stereotype.Service;
10
11import javax.crypto.SecretKey;
12import java.util.Date;
13import java.util.HashMap;
14import java.util.Map;
15import java.util.function.Function;
16
17@Service
18public class JwtService {
19
20 @Value("${jwt.secret}")
21 private String secretKey;
22
23 @Value("${jwt.expiration}")
24 private long jwtExpiration;
25
26 /**
27 * Extrae el username (subject) del token.
28 */
29 public String extractUsername(String token) {
30 return extractClaim(token, Claims::getSubject);
31 }
32
33 /**
34 * Extrae un claim específico del token usando un resolver.
35 */
36 public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
37 final Claims claims = extractAllClaims(token);
38 return claimsResolver.apply(claims);
39 }
40
41 /**
42 * Genera un token JWT para el usuario dado.
43 */
44 public String generateToken(UserDetails userDetails) {
45 return generateToken(new HashMap<>(), userDetails);
46 }
47
48 /**
49 * Genera un token JWT con claims adicionales.
50 */
51 public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
52 return buildToken(extraClaims, userDetails, jwtExpiration);
53 }
54
55 /**
56 * Valida que el token pertenezca al usuario y no haya expirado.
57 */
58 public boolean isTokenValid(String token, UserDetails userDetails) {
59 final String username = extractUsername(token);
60 return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
61 }
62
63 /**
64 * Construye el token JWT con los parámetros dados.
65 * Usa la API de JJWT 0.12.x con el builder actualizado.
66 */
67 private String buildToken(
68 Map<String, Object> extraClaims,
69 UserDetails userDetails,
70 long expiration
71 ) {
72 return Jwts.builder()
73 .claims(extraClaims)
74 .subject(userDetails.getUsername())
75 .issuedAt(new Date(System.currentTimeMillis()))
76 .expiration(new Date(System.currentTimeMillis() + expiration))
77 .signWith(getSigningKey())
78 .compact();
79 }
80
81 /**
82 * Verifica si el token ha expirado.
83 */
84 private boolean isTokenExpired(String token) {
85 return extractExpiration(token).before(new Date());
86 }
87
88 /**
89 * Extrae la fecha de expiración del token.
90 */
91 private Date extractExpiration(String token) {
92 return extractClaim(token, Claims::getExpiration);
93 }
94
95 /**
96 * Extrae todos los claims del token.
97 * Usa el parser actualizado de JJWT 0.12.x.
98 */
99 private Claims extractAllClaims(String token) {
100 return Jwts.parser()
101 .verifyWith(getSigningKey())
102 .build()
103 .parseSignedClaims(token)
104 .getPayload();
105 }
106
107 /**
108 * Construye la clave de firma a partir del secreto en Base64.
109 */
110 private SecretKey getSigningKey() {
111 byte[] keyBytes = Decoders.BASE64.decode(secretKey);
112 return Keys.hmacShaKeyFor(keyBytes);
113 }
114}
JJWT 0.12.x vs versiones anteriores: Si has usado JJWT 0.11.x, notarás cambios importantes. El método setSubject() ahora es subject(), setIssuedAt() es issuedAt(), setExpiration() es expiration(), y signWith(key, algorithm) ahora infiere el algoritmo automáticamente con signWith(key). El parser usa Jwts.parser().verifyWith(key) en lugar de Jwts.parserBuilder().setSigningKey(key).
Implementando el Filtro JWT
El filtro JWT intercepta cada petición HTTP, extrae el token del header Authorization, lo valida y establece la autenticación en el SecurityContext de Spring. Extiende OncePerRequestFilter para garantizar que se ejecuta una sola vez por petición.
1package com.example.jwtsecuritydemo.filter;
2
3import com.example.jwtsecuritydemo.service.JwtService;
4import jakarta.servlet.FilterChain;
5import jakarta.servlet.ServletException;
6import jakarta.servlet.http.HttpServletRequest;
7import jakarta.servlet.http.HttpServletResponse;
8import org.springframework.lang.NonNull;
9import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10import org.springframework.security.core.context.SecurityContextHolder;
11import org.springframework.security.core.userdetails.UserDetails;
12import org.springframework.security.core.userdetails.UserDetailsService;
13import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
14import org.springframework.stereotype.Component;
15import org.springframework.web.filter.OncePerRequestFilter;
16
17import java.io.IOException;
18
19@Component
20public class JwtAuthenticationFilter extends OncePerRequestFilter {
21
22 private final JwtService jwtService;
23 private final UserDetailsService userDetailsService;
24
25 public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
26 this.jwtService = jwtService;
27 this.userDetailsService = userDetailsService;
28 }
29
30 @Override
31 protected void doFilterInternal(
32 @NonNull HttpServletRequest request,
33 @NonNull HttpServletResponse response,
34 @NonNull FilterChain filterChain
35 ) throws ServletException, IOException {
36
37 // 1. Extraer el header Authorization
38 final String authHeader = request.getHeader("Authorization");
39
40 // 2. Si no hay header o no empieza con "Bearer ", continuar la cadena
41 if (authHeader == null || !authHeader.startsWith("Bearer ")) {
42 filterChain.doFilter(request, response);
43 return;
44 }
45
46 // 3. Extraer el token (quitar "Bearer ")
47 final String jwt = authHeader.substring(7);
48 final String userEmail = jwtService.extractUsername(jwt);
49
50 // 4. Si tenemos username y no hay autenticación previa en el contexto
51 if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
52
53 // 5. Cargar el usuario desde la base de datos
54 UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
55
56 // 6. Validar el token contra los datos del usuario
57 if (jwtService.isTokenValid(jwt, userDetails)) {
58
59 // 7. Crear el objeto de autenticación
60 UsernamePasswordAuthenticationToken authToken =
61 new UsernamePasswordAuthenticationToken(
62 userDetails,
63 null,
64 userDetails.getAuthorities()
65 );
66
67 // 8. Agregar detalles de la petición
68 authToken.setDetails(
69 new WebAuthenticationDetailsSource().buildDetails(request)
70 );
71
72 // 9. Establecer la autenticación en el SecurityContext
73 SecurityContextHolder.getContext().setAuthentication(authToken);
74 }
75 }
76
77 // 10. Continuar la cadena de filtros
78 filterChain.doFilter(request, response);
79 }
80}
El flujo del filtro es el siguiente:
La petición llega al filtro antes de alcanzar cualquier controller.
Se busca el header
Authorizationcon formatoBearer <token>.Se extrae el username del token.
Se carga el usuario de la base de datos y se valida que el token sea correcto y no haya expirado.
Si todo es válido, se establece la autenticación en el
SecurityContext, permitiendo el acceso a los endpoints protegidos.
Configuración de Spring Security
La clase de configuración de seguridad define qué endpoints son públicos, cuáles requieren autenticación, y cómo se integra nuestro filtro JWT en la cadena de seguridad de Spring.
1package com.example.jwtsecuritydemo.config;
2
3import com.example.jwtsecuritydemo.filter.JwtAuthenticationFilter;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6import org.springframework.security.authentication.AuthenticationManager;
7import org.springframework.security.authentication.AuthenticationProvider;
8import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
9import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
10import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
13import org.springframework.security.config.http.SessionCreationPolicy;
14import org.springframework.security.core.userdetails.UserDetailsService;
15import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
16import org.springframework.security.crypto.password.PasswordEncoder;
17import org.springframework.security.web.SecurityFilterChain;
18import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
19
20@Configuration
21@EnableWebSecurity
22public class SecurityConfig {
23
24 private final JwtAuthenticationFilter jwtAuthFilter;
25 private final UserDetailsService userDetailsService;
26
27 public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
28 UserDetailsService userDetailsService) {
29 this.jwtAuthFilter = jwtAuthFilter;
30 this.userDetailsService = userDetailsService;
31 }
32
33 @Bean
34 public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
35 return http
36 // Deshabilitar CSRF (no necesario con JWT stateless)
37 .csrf(AbstractHttpConfigurer::disable)
38 // Configurar autorización de endpoints
39 .authorizeHttpRequests(auth -> auth
40 .requestMatchers("/auth/**").permitAll()
41 .requestMatchers("/h2-console/**").permitAll()
42 .anyRequest().authenticated()
43 )
44 // Configurar sesión stateless
45 .sessionManagement(session -> session
46 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
47 )
48 // Registrar el proveedor de autenticación
49 .authenticationProvider(authenticationProvider())
50 // Agregar filtro JWT antes del filtro de Spring Security
51 .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
52 // Permitir frames para H2 Console
53 .headers(headers -> headers
54 .frameOptions(frame -> frame.sameOrigin())
55 )
56 .build();
57 }
58
59 @Bean
60 public AuthenticationProvider authenticationProvider() {
61 DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
62 authProvider.setUserDetailsService(userDetailsService);
63 authProvider.setPasswordEncoder(passwordEncoder());
64 return authProvider;
65 }
66
67 @Bean
68 public AuthenticationManager authenticationManager(
69 AuthenticationConfiguration config
70 ) throws Exception {
71 return config.getAuthenticationManager();
72 }
73
74 @Bean
75 public PasswordEncoder passwordEncoder() {
76 return new BCryptPasswordEncoder();
77 }
78}
¿Por qué deshabilitamos CSRF? La protección CSRF es esencial en aplicaciones basadas en sesiones/cookies, pero con JWT stateless no es necesaria. El token se envía en el header Authorization, que un atacante no puede inyectar automáticamente desde un sitio malicioso (a diferencia de las cookies que se envían automáticamente).
Endpoint de Autenticación
Ahora vamos a crear la entidad de usuario, el repositorio, el servicio de autenticación y el controller con los endpoints de login y registro.
Entidad de Usuario
1package com.example.jwtsecuritydemo.entity;
2
3import jakarta.persistence.*;
4import org.springframework.security.core.GrantedAuthority;
5import org.springframework.security.core.authority.SimpleGrantedAuthority;
6import org.springframework.security.core.userdetails.UserDetails;
7
8import java.util.Collection;
9import java.util.List;
10
11@Entity
12@Table(name = "app_user")
13public class UserEntity implements UserDetails {
14
15 @Id
16 @GeneratedValue(strategy = GenerationType.IDENTITY)
17 private Long id;
18
19 @Column(nullable = false)
20 private String firstName;
21
22 @Column(nullable = false)
23 private String lastName;
24
25 @Column(nullable = false, unique = true)
26 private String email;
27
28 @Column(nullable = false)
29 private String password;
30
31 @Enumerated(EnumType.STRING)
32 @Column(nullable = false)
33 private Role role;
34
35 public enum Role {
36 USER, ADMIN
37 }
38
39 // Constructor vacío requerido por JPA
40 public UserEntity() {}
41
42 public UserEntity(String firstName, String lastName, String email,
43 String password, Role role) {
44 this.firstName = firstName;
45 this.lastName = lastName;
46 this.email = email;
47 this.password = password;
48 this.role = role;
49 }
50
51 // --- Implementación de UserDetails ---
52
53 @Override
54 public Collection<? extends GrantedAuthority> getAuthorities() {
55 return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
56 }
57
58 @Override
59 public String getUsername() {
60 return email; // Usamos email como username
61 }
62
63 @Override
64 public String getPassword() {
65 return password;
66 }
67
68 @Override
69 public boolean isAccountNonExpired() { return true; }
70
71 @Override
72 public boolean isAccountNonLocked() { return true; }
73
74 @Override
75 public boolean isCredentialsNonExpired() { return true; }
76
77 @Override
78 public boolean isEnabled() { return true; }
79
80 // --- Getters y Setters ---
81 public Long getId() { return id; }
82 public String getFirstName() { return firstName; }
83 public String getLastName() { return lastName; }
84 public String getEmail() { return email; }
85 public Role getRole() { return role; }
86}
Repositorio
1package com.example.jwtsecuritydemo.repository;
2
3import com.example.jwtsecuritydemo.entity.UserEntity;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6import java.util.Optional;
7
8public interface UserRepository extends JpaRepository<UserEntity, Long> {
9 Optional<UserEntity> findByEmail(String email);
10 boolean existsByEmail(String email);
11}
DTOs con Records de Java 17
1// RegisterRequest.java
2package com.example.jwtsecuritydemo.dto;
3
4import jakarta.validation.constraints.Email;
5import jakarta.validation.constraints.NotBlank;
6import jakarta.validation.constraints.Size;
7
8public record RegisterRequest(
9 @NotBlank(message = "El nombre es obligatorio")
10 String firstName,
11
12 @NotBlank(message = "El apellido es obligatorio")
13 String lastName,
14
15 @NotBlank(message = "El email es obligatorio")
16 @Email(message = "El email debe tener un formato válido")
17 String email,
18
19 @NotBlank(message = "La contraseña es obligatoria")
20 @Size(min = 8, message = "La contraseña debe tener al menos 8 caracteres")
21 String password
22) {}
23
24// LoginRequest.java
25package com.example.jwtsecuritydemo.dto;
26
27import jakarta.validation.constraints.Email;
28import jakarta.validation.constraints.NotBlank;
29
30public record LoginRequest(
31 @NotBlank @Email String email,
32 @NotBlank String password
33) {}
34
35// AuthResponse.java
36package com.example.jwtsecuritydemo.dto;
37
38public record AuthResponse(
39 String token,
40 String type,
41 String email
42) {
43 public AuthResponse(String token, String email) {
44 this(token, "Bearer", email);
45 }
46}
Servicio de Autenticación
1package com.example.jwtsecuritydemo.service;
2
3import com.example.jwtsecuritydemo.dto.AuthResponse;
4import com.example.jwtsecuritydemo.dto.LoginRequest;
5import com.example.jwtsecuritydemo.dto.RegisterRequest;
6import com.example.jwtsecuritydemo.entity.UserEntity;
7import com.example.jwtsecuritydemo.repository.UserRepository;
8import org.springframework.security.authentication.AuthenticationManager;
9import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10import org.springframework.security.crypto.password.PasswordEncoder;
11import org.springframework.stereotype.Service;
12
13@Service
14public class AuthService {
15
16 private final UserRepository userRepository;
17 private final JwtService jwtService;
18 private final PasswordEncoder passwordEncoder;
19 private final AuthenticationManager authenticationManager;
20
21 public AuthService(UserRepository userRepository, JwtService jwtService,
22 PasswordEncoder passwordEncoder,
23 AuthenticationManager authenticationManager) {
24 this.userRepository = userRepository;
25 this.jwtService = jwtService;
26 this.passwordEncoder = passwordEncoder;
27 this.authenticationManager = authenticationManager;
28 }
29
30 public AuthResponse register(RegisterRequest request) {
31 // Verificar si el email ya existe
32 if (userRepository.existsByEmail(request.email())) {
33 throw new IllegalArgumentException("El email ya está registrado");
34 }
35
36 // Crear el usuario
37 var user = new UserEntity(
38 request.firstName(),
39 request.lastName(),
40 request.email(),
41 passwordEncoder.encode(request.password()),
42 UserEntity.Role.USER
43 );
44
45 userRepository.save(user);
46
47 // Generar el token JWT
48 String token = jwtService.generateToken(user);
49 return new AuthResponse(token, user.getEmail());
50 }
51
52 public AuthResponse login(LoginRequest request) {
53 // Autenticar con Spring Security
54 authenticationManager.authenticate(
55 new UsernamePasswordAuthenticationToken(
56 request.email(),
57 request.password()
58 )
59 );
60
61 // Si la autenticación es exitosa, buscar el usuario
62 var user = userRepository.findByEmail(request.email())
63 .orElseThrow(() -> new IllegalArgumentException("Usuario no encontrado"));
64
65 // Generar el token JWT
66 String token = jwtService.generateToken(user);
67 return new AuthResponse(token, user.getEmail());
68 }
69}
Controller de Autenticación
1package com.example.jwtsecuritydemo.controller;
2
3import com.example.jwtsecuritydemo.dto.AuthResponse;
4import com.example.jwtsecuritydemo.dto.LoginRequest;
5import com.example.jwtsecuritydemo.dto.RegisterRequest;
6import com.example.jwtsecuritydemo.service.AuthService;
7import jakarta.validation.Valid;
8import org.springframework.http.ResponseEntity;
9import org.springframework.web.bind.annotation.*;
10
11@RestController
12@RequestMapping("/auth")
13public class AuthController {
14
15 private final AuthService authService;
16
17 public AuthController(AuthService authService) {
18 this.authService = authService;
19 }
20
21 @PostMapping("/register")
22 public ResponseEntity<AuthResponse> register(
23 @Valid @RequestBody RegisterRequest request
24 ) {
25 return ResponseEntity.ok(authService.register(request));
26 }
27
28 @PostMapping("/login")
29 public ResponseEntity<AuthResponse> login(
30 @Valid @RequestBody LoginRequest request
31 ) {
32 return ResponseEntity.ok(authService.login(request));
33 }
34}
35
36// --- Endpoint protegido de ejemplo ---
37package com.example.jwtsecuritydemo.controller;
38
39import org.springframework.http.ResponseEntity;
40import org.springframework.security.core.Authentication;
41import org.springframework.web.bind.annotation.GetMapping;
42import org.springframework.web.bind.annotation.RequestMapping;
43import org.springframework.web.bind.annotation.RestController;
44
45import java.util.Map;
46
47@RestController
48@RequestMapping("/api")
49public class DemoController {
50
51 @GetMapping("/profile")
52 public ResponseEntity<Map<String, String>> getProfile(Authentication authentication) {
53 return ResponseEntity.ok(Map.of(
54 "message", "Bienvenido a tu perfil protegido",
55 "user", authentication.getName(),
56 "authorities", authentication.getAuthorities().toString()
57 ));
58 }
59}
No olvides configurar el UserDetailsService para que Spring Security pueda cargar los usuarios:
1package com.example.jwtsecuritydemo.config;
2
3import com.example.jwtsecuritydemo.repository.UserRepository;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6import org.springframework.security.core.userdetails.UserDetailsService;
7import org.springframework.security.core.userdetails.UsernameNotFoundException;
8
9@Configuration
10public class ApplicationConfig {
11
12 private final UserRepository userRepository;
13
14 public ApplicationConfig(UserRepository userRepository) {
15 this.userRepository = userRepository;
16 }
17
18 @Bean
19 public UserDetailsService userDetailsService() {
20 return username -> userRepository.findByEmail(username)
21 .orElseThrow(() ->
22 new UsernameNotFoundException("Usuario no encontrado: " + username));
23 }
24}
Probando la API
Con todo configurado, ejecuta la aplicación y prueba los endpoints con curl o cualquier cliente HTTP como Postman o Insomnia.
1. Registrar un usuario
1curl -X POST http://localhost:8080/auth/register \
2 -H "Content-Type: application/json" \
3 -d '{
4 "firstName": "Ana",
5 "lastName": "García",
6 "email": "[email protected]",
7 "password": "MiPassword123"
8 }'
Respuesta esperada:
1{
2 "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbmEuZ2FyY2lhQGV4YW1wbGUuY29tIiwiaWF0IjoxNzEyMDAwMDAwLCJleHAiOjE3MTIwMDM2MDB9.xxxxx",
3 "type": "Bearer",
4 "email": "[email protected]"
5}
2. Iniciar sesión
1curl -X POST http://localhost:8080/auth/login \
2 -H "Content-Type: application/json" \
3 -d '{
4 "email": "[email protected]",
5 "password": "MiPassword123"
6 }'
3. Acceder a un endpoint protegido
1# Reemplaza <TU_TOKEN> con el token recibido en la respuesta anterior
2curl -X GET http://localhost:8080/api/profile \
3 -H "Authorization: Bearer <TU_TOKEN>"
Respuesta esperada:
1{
2 "message": "Bienvenido a tu perfil protegido",
3 "user": "[email protected]",
4 "authorities": "[ROLE_USER]"
5}
4. Verificar que sin token se rechaza la petición
1curl -X GET http://localhost:8080/api/profile
2# Respuesta: 403 Forbidden
Depuración: Si recibes un 403 inesperado, verifica que: (1) el token no haya expirado, (2) el header sea exactamente Authorization: Bearer <token> con un espacio después de "Bearer", y (3) la clave secreta sea la misma para generar y validar.
Próximos Pasos
En este primer artículo construimos los fundamentos completos de autenticación JWT en Spring Boot 3:
Entendimos la estructura y funcionamiento de los JSON Web Tokens.
Configuramos un proyecto Spring Boot con todas las dependencias necesarias.
Creamos un
JwtServiceusando la API moderna de JJWT 0.12.x.Implementamos un filtro JWT que intercepta y valida cada petición.
Configuramos Spring Security en modo stateless.
Creamos endpoints de registro e inicio de sesión funcionales.
Sin embargo, nuestro sistema tiene una limitación importante: cuando el access token expira, el usuario debe volver a iniciar sesión. En una aplicación real, esto sería una pésima experiencia de usuario.
En la Parte 2 resolveremos este problema implementando refresh tokens. Aprenderás a: almacenar refresh tokens en la base de datos, crear un endpoint /auth/refresh para renovar tokens sin re-autenticarse, implementar rotación de tokens para mayor seguridad, y manejar el flujo completo de renovación desde el frontend.
Si este artículo te fue útil, compártelo con otros desarrolladores Java. ¡Nos vemos en la siguiente parte del curso!
Comments
Sign in to leave a comment
No comments yet. Be the first!
Related Articles
Stay updated
Get notified when I publish new articles. No spam, unsubscribe anytime.