Cómo construir una API REST segura y eficiente en Java con Spring Boot y JWT
Tutorial práctico: implementa una API REST con autenticación JWT, separación de capas, validaciones y protección de endpoints. Incluye estructura de proyecto, código clave y explicaciones "por qué".
Requisitos
- Java 17+
- Maven o Gradle (usaré Maven)
- Spring Boot 3.x
- Postman / curl para pruebas
Estructura de carpetas
src/main/java/com/example/demo
├── DemoApplication.java
├── config
│ └── SecurityConfig.java
├── controller
│ ├── AuthController.java
│ └── UserController.java
├── dto
│ ├── AuthRequest.java
│ └── AuthResponse.java
├── entity
│ └── AppUser.java
├── repository
│ └── UserRepository.java
├── security
│ ├── JwtFilter.java
│ └── JwtUtil.java
└── service
└── UserService.java
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<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-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
jwt.secret=mi_clave_secreta_muy_larga_y_segura
jwt.expiration-ms=3600000
Entidad AppUser
package com.example.demo.entity;
import jakarta.persistence.*;
@Entity
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password; // hashed
// getters y setters
}
Repositorio
package com.example.demo.repository;
import com.example.demo.entity.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}
DTOs de autenticación
package com.example.demo.dto;
public record AuthRequest(String username, String password) {}
public record AuthResponse(String token) {}
JwtUtil (crear y validar tokens)
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtil {
private final Key key;
private final long expirationMs;
public JwtUtil(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration-ms}") long expirationMs) {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
this.expirationMs = expirationMs;
}
public String generateToken(String username) {
Date now = new Date();
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expirationMs))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
JwtFilter (extrae token y autentica)
package com.example.demo.security;
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.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
var auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
Servicio de usuario (UserDetailsService + registro)
package com.example.demo.service;
import com.example.demo.entity.AppUser;
import com.example.demo.repository.UserRepository;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
private final UserRepository repo;
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public UserService(UserRepository repo) { this.repo = repo; }
public AppUser register(String username, String rawPassword) {
var u = new AppUser();
u.setUsername(username);
u.setPassword(encoder.encode(rawPassword));
return repo.save(u);
}
@Override
public UserDetails loadUserByUsername(String username) {
AppUser u = repo.findByUsername(username).orElseThrow();
return User.withUsername(u.getUsername()).password(u.getPassword()).roles("USER").build();
}
}
SecurityConfig
package com.example.demo.config;
import com.example.demo.security.JwtFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) { this.jwtFilter = jwtFilter; }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
AuthController
package com.example.demo.controller;
import com.example.demo.dto.AuthRequest;
import com.example.demo.dto.AuthResponse;
import com.example.demo.service.UserService;
import com.example.demo.security.JwtUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final UserService userService;
private final AuthenticationManager authManager;
private final JwtUtil jwtUtil;
public AuthController(UserService userService, AuthenticationManager authManager, JwtUtil jwtUtil) {
this.userService = userService;
this.authManager = authManager;
this.jwtUtil = jwtUtil;
}
@PostMapping("/register")
public String register(@RequestBody AuthRequest req) {
userService.register(req.username(), req.password());
return "usuario registrado";
}
@PostMapping("/login")
public AuthResponse login(@RequestBody AuthRequest req) {
Authentication auth = authManager.authenticate(new UsernamePasswordAuthenticationToken(req.username(), req.password()));
String token = jwtUtil.generateToken(req.username());
return new AuthResponse(token);
}
}
Controlador protegido de ejemplo
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/profile")
public String profile() {
return "Perfil protegido";
}
}
Pruebas (curl)
# Registrar
curl -X POST -H "Content-Type: application/json" -d '{"username":"juan","password":"1234"}' http://localhost:8080/auth/register
# Login -> obtiene token
curl -X POST -H "Content-Type: application/json" -d '{"username":"juan","password":"1234"}' http://localhost:8080/auth/login
# Usar token
curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/profile
Por qué estas decisiones
- JWT: stateless, escalable (evita sesión en servidor). Usa expiración y clave fuerte.
- BCrypt para contraseñas: resistente a ataques por fuerza bruta y salt integrado.
- OncePerRequestFilter para insertar la autenticación antes del filtro de Spring Security.
- Separación de capas: controller / service / repository para testabilidad y mantenimiento.
Consideraciones de seguridad
- No almacenes la clave JWT en el código; usa secretos desde un vault o variables de entorno.
- Implementa rotación de claves y revocación de tokens (p. ej. listas de revocación o short-lived + refresh tokens).
- Valida inputs y aplica límites de tasa para evitar fuerza bruta.
Consejo avanzado: añade refresh tokens con almacenamiento seguro (HTTP-only cookies) y maneja la revocación en una tabla con expiración para poder invalidar tokens sin perder la escalabilidad.
Advertencia: un JWT comprometido permite acceso hasta su expiración; minimizar el tiempo de vida y monitorizar actividad inusual es imprescindible.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación