Asegurando tus Aplicaciones Java: Implementando Autenticación JWT con Spring Security
Este tutorial te guiará paso a paso en la implementación de autenticación JSON Web Token (JWT) en una aplicación Java usando Spring Security. Aprenderás a configurar Spring Security, generar y validar JWTs, y proteger tus endpoints de API.
🚀 Introducción a la Seguridad en Aplicaciones Java
En el desarrollo de aplicaciones modernas, la seguridad es un pilar fundamental. Proteger los datos y los recursos de accesos no autorizados es una prioridad. Cuando hablamos de APIs REST, el modelo tradicional de sesiones con cookies puede no ser la solución más eficiente o escalable, especialmente en arquitecturas de microservicios o aplicaciones frontend-backend desacopladas.
Aquí es donde entra en juego JSON Web Token (JWT). JWT es un estándar abierto (RFC 7519) que define una forma compacta y autocontenida de transmitir información de forma segura entre partes como un objeto JSON. Esta información puede ser verificada y de confianza porque está firmada digitalmente.
¿Por qué JWT para tus APIs REST?
- Sin estado (Stateless): Los servidores no necesitan almacenar el estado de la sesión, lo que mejora la escalabilidad.
- Compacto: El tamaño reducido de los tokens permite transmitirlos eficientemente.
- Autocontenido: El token contiene toda la información necesaria sobre el usuario, eliminando la necesidad de consultas adicionales a la base de datos para cada solicitud.
- Seguro: La firma digital garantiza la integridad y autenticidad del token.
- Interoperable: Puede ser utilizado en diferentes lenguajes y plataformas.
🛠️ Configuración del Entorno de Desarrollo
Para este tutorial, utilizaremos Spring Boot, que simplifica enormemente la configuración de aplicaciones Java. Necesitarás tener instalado:
- Java Development Kit (JDK): Versión 11 o superior.
- Maven o Gradle: Para la gestión de dependencias.
- IDE: IntelliJ IDEA, Eclipse o VS Code.
1. Creando el Proyecto Spring Boot
Empezaremos creando un nuevo proyecto Spring Boot. Puedes usar Spring Initializr o tu IDE. Asegúrate de incluir las siguientes dependencias:
- Spring Web: Para construir APIs REST.
- Spring Security: Para la autenticación y autorización.
- Lombok: (Opcional, pero recomendado) Para reducir el boilerplate code.
- java-jwt (Auth0): Para la generación y validación de JWT.
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-jwt-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-jwt-security</name>
<description>Demo project for Spring Boot JWT Security</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
🔑 Implementando la Lógica de JWT
Primero, definiremos algunas constantes para nuestro JWT, como la clave secreta y el tiempo de expiración. Estas se guardarán en application.properties por simplicidad, pero en un entorno de producción, deberían ser gestionadas por un servicio de secretos.
src/main/resources/application.properties
jwt.secret=UnaClaveSecretaMuyFuerteQueDeberiaEstarEnUnServicioDeSecretosPeroParaElTutorialEstaAqui
jwt.expiration.minutes=60
1. Clase para la Generación y Validación de JWT
Crearemos una clase de utilidad que se encargará de generar y validar los tokens JWT. Utilizaremos la librería com.auth0:java-jwt.
src/main/java/com/example/springjwtsecurity/util/JwtUtil.java
package com.example.springjwtsecurity.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration.minutes}")
private long expirationMinutes;
public String generateToken(UserDetails userDetails) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
return JWT.create()
.withSubject(userDetails.getUsername())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(expirationMinutes)))
.sign(algorithm);
} catch (JWTCreationException exception){
// Invalid Signing configuration / Error during serialization/deserialization of the JWT
throw new RuntimeException("Error al generar JWT", exception);
}
}
public String extractUsername(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.build(); // Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
return jwt.getSubject();
} catch (JWTVerificationException exception){
// Invalid signature/claims
throw new RuntimeException("Error al validar JWT o extraer username", exception);
}
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getExpiresAt().before(new Date());
} catch (JWTVerificationException exception) {
return true; // Si hay error en la verificación, consideramos el token expirado o inválido
}
}
}
2. Servicio de Detalles de Usuario (UserDetailsService)
Spring Security necesita una forma de cargar los detalles del usuario durante el proceso de autenticación. Crearemos un servicio que implemente UserDetailsService.
src/main/java/com/example/springjwtsecurity/service/UserDetailsServiceImpl.java
package com.example.springjwtsecurity.service;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
// Simulación de base de datos de usuarios
private final Map<String, UserDetails> users = new HashMap<>();
public UserDetailsServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
// Añadir usuarios de prueba. En un entorno real, esto vendría de una DB.
users.put("user", new User("user", passwordEncoder.encode("password"), new ArrayList<>()));
users.put("admin", new User("admin", passwordEncoder.encode("adminpass"), new ArrayList<>()));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = users.get(username);
if (user == null) {
throw new UsernameNotFoundException("Usuario no encontrado: " + username);
}
return user;
}
}
🛡️ Configurando Spring Security
La configuración de Spring Security es el corazón de nuestro sistema de autenticación JWT. Necesitaremos desactivar la autenticación de sesión por defecto de Spring Security y añadir un filtro para procesar los JWTs.
1. Filtro de Autenticación JWT
Este filtro interceptará cada solicitud HTTP, extraerá el JWT del encabezado Authorization, lo validará y autenticará al usuario si el token es válido.
src/main/java/com/example/springjwtsecurity/filter/JwtRequestFilter.java
package com.example.springjwtsecurity.filter;
import com.example.springjwtsecurity.service.UserDetailsServiceImpl;
import com.example.springjwtsecurity.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
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.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsServiceImpl userDetailsService;
private final JwtUtil jwtUtil;
public JwtRequestFilter(UserDetailsServiceImpl userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (RuntimeException e) {
logger.error("JWT malformado o inválido: " + e.getMessage());
// Podrías lanzar una excepción o simplemente ignorar para dejar que Spring Security la maneje
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
2. Configuración Principal de Spring Security
Crearemos una clase de configuración que extenderá de WebSecurityConfigurerAdapter (para versiones anteriores de Spring Security) o, como haremos aquí para Spring Security 6+, utilizaremos un SecurityFilterChain Bean.
src/main/java/com/example/springjwtsecurity/config/SecurityConfig.java
package com.example.springjwtsecurity.config;
import com.example.springjwtsecurity.filter.JwtRequestFilter;
import com.example.springjwtsecurity.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final JwtRequestFilter jwtRequestFilter;
public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtRequestFilter jwtRequestFilter) {
this.userDetailsService = userDetailsService;
this.jwtRequestFilter = jwtRequestFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(UserDetailsServiceImpl userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authenticationProvider);
}
}
3. Diagrama de Flujo de Autenticación JWT
🌐 Creando los Endpoints de API
Ahora crearemos un controlador para manejar la solicitud de autenticación y un endpoint protegido para verificar que nuestra seguridad funciona.
1. Modelo de Solicitud de Autenticación
Crearemos una clase simple para representar las credenciales del usuario en la solicitud de autenticación.
src/main/java/com/example/springjwtsecurity/model/AuthenticationRequest.java
package com.example.springjwtsecurity.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationRequest {
private String username;
private String password;
}
2. Controlador de Autenticación
Este controlador expone un endpoint /authenticate que recibe las credenciales del usuario, las autentica y devuelve un JWT si son válidas.
src/main/java/com/example/springjwtsecurity/controller/AuthController.java
package com.example.springjwtsecurity.controller;
import com.example.springjwtsecurity.model.AuthenticationRequest;
import com.example.springjwtsecurity.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("Credenciales incorrectas", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(jwt);
}
}
3. Endpoint Protegido
Para probar nuestra configuración, crearemos un endpoint simple que solo sea accesible si el usuario está autenticado.
src/main/java/com/example/springjwtsecurity/controller/ResourceController.java
package com.example.springjwtsecurity.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@GetMapping("/hello")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("¡Hola! Has accedido a un recurso protegido.");
}
}
🧪 Probando la Autenticación JWT
Para probar nuestra aplicación, puedes usar herramientas como Postman, Insomnia o curl.
1. Obtener un JWT
Realiza una solicitud POST a /authenticate con las credenciales de un usuario. Por ejemplo, user/password o admin/adminpass.
Solicitud:
POST http://localhost:8080/authenticate
Content-Type: application/json
{
"username": "user",
"password": "password"
}
Respuesta (ejemplo):
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 175
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjcyNTI5MDMxLCJleHAiOjE2NzI1MzI2MzF9.uXyPz_eW7kM_SgA-vB7mZ9rT_hM_0wK_9mX_2Xz_4kI
Copia el token JWT de la respuesta.
2. Acceder a un Recurso Protegido
Ahora, usa el token que obtuviste para acceder al endpoint /hello.
Solicitud:
GET http://localhost:8080/hello
Authorization: Bearer <TU_JWT_AQUI>
Reemplaza <TU_JWT_AQUI> con el token real.
Respuesta esperada:
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 46
¡Hola! Has accedido a un recurso protegido.
3. Intentar Acceder sin Token o con Token Inválido
Si intentas acceder a /hello sin un token o con un token inválido/expirado, deberías recibir un error de autenticación.
Solicitud (sin token):
GET http://localhost:8080/hello
Respuesta esperada:
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: ...
📈 Mejoras y Consideraciones Adicionales
Esta implementación proporciona una base sólida, pero hay varias áreas donde se puede mejorar y consideraciones importantes para un entorno de producción.
1. Refrescar Tokens
Los JWT tienen un tiempo de expiración. Para mejorar la experiencia del usuario y la seguridad, puedes implementar un mecanismo de tokens de refresco (refresh tokens). Cuando un access token expira, la aplicación cliente puede usar un refresh token (de mayor duración y almacenado de forma más segura) para obtener un nuevo access token sin necesidad de que el usuario vuelva a iniciar sesión.
2. Listas Negras de Tokens (Blacklisting)
Dado que los JWT son sin estado, no hay una forma inherente de 'revocar' un token antes de su expiración. Si un token se ve comprometido o un usuario cierra sesión, el token sigue siendo válido hasta que expira. Para mitigar esto, se puede implementar una 'lista negra' de tokens. Cuando un usuario cierra sesión o un token debe ser invalidado, su ID (JTI) se añade a una base de datos o caché (ej. Redis) con una fecha de expiración igual a la del token. Cada vez que se recibe un token, se verifica si está en la lista negra.
3. Roles y Autorización
Para implementar autorización (qué recursos puede acceder un usuario autenticado), puedes incluir roles o permisos en el payload del JWT. Spring Security permite definir reglas de autorización basadas en roles en tus endpoints (ej. @PreAuthorize("hasRole('ADMIN')")).
Ejemplo de Roles en JWT Payload
Podrías añadir los roles al generar el token:
public String generateToken(UserDetails userDetails) {
// ... otros claims ...
String roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return JWT.create()
.withSubject(userDetails.getUsername())
.withClaim("roles", roles) // Añade los roles como un claim
// ... otros claims ...
.sign(algorithm);
}
Y luego configurar Spring Security para extraer los roles de SecurityContextHolder o usar una implementación personalizada de GrantedAuthoritiesMapper.
4. Gestión de Errores y Excepciones
Es fundamental tener una gestión robusta de errores. Define excepciones personalizadas y utiliza un @ControllerAdvice para manejar globalmente los errores de autenticación y autorización, devolviendo respuestas JSON consistentes y útiles para el cliente.
5. Almacenamiento Seguro del JWT en el Cliente
En el cliente (ej. aplicación web frontend), el JWT debe almacenarse de forma segura. Opciones comunes incluyen localStorage, sessionStorage o cookies HTTP-only. Cada método tiene sus pros y contras en términos de seguridad contra ataques XSS y CSRF.
✅ Conclusión
Has llegado al final de este tutorial sobre la implementación de autenticación JWT con Spring Security en Java. Hemos cubierto desde la configuración inicial del proyecto hasta la generación, validación y prueba de tokens, sentando las bases para proteger tus APIs REST de manera efectiva.
La seguridad es un campo vasto y en constante evolución. Te animo a seguir investigando y profundizando en temas como la gestión de refresh tokens, la implementación de listas negras, la integración con proveedores de identidad (OAuth2/OIDC) y las mejores prácticas de seguridad para tus aplicaciones Java.
Tutoriales relacionados
- Optimización del Rendimiento en Aplicaciones Java con JVM y Garbage Collectionintermediate20 min
- Dominando la Programación Asíncrona en Java con CompletableFutureintermediate20 min
- Explorando y Diseñando APIs RESTful en Java con Spring Boot: Guía Prácticaintermediate20 min
- Gestionando la Conectividad a Bases de Datos en Java con el Pool de Conexiones HikariCPintermediate15 min
- Dominando la Persistencia de Datos en Java: Hibernate y JPA desde Cerointermediate35 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!