Proyecto práctico: API de autenticación segura con Spring Boot, JWT y BCrypt
Objetivo: crear una API REST en Java (Spring Boot) que ofrezca registro, login y protección de endpoints usando JWT para autenticación stateless y BCrypt para hash de contraseñas. Incluyo código completo de los archivos principales, estructura de proyecto y explicaciones del porqué.
Requisitos previos
- Java 17+
- Maven
- IDE (IntelliJ, VSCode)
- Conocimientos básicos de Spring Boot y Maven
Estructura de carpetas
spring-jwt-auth/
├─ pom.xml
└─ src/main/java/com/example/demo/
├─ DemoApplication.java
├─ controller/
│ └─ AuthController.java
├─ dto/
│ ├─ AuthRequest.java
│ ├─ AuthResponse.java
│ └─ RegisterRequest.java
├─ model/
│ └─ User.java
├─ repository/
│ └─ UserRepository.java
├─ service/
│ └─ UserService.java
└─ security/
├─ SecurityConfig.java
├─ JwtUtil.java
└─ JwtFilter.java
src/main/resources/
├─ application.properties
pom.xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-jwt-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<spring.boot.version>3.1.4</spring.boot.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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
# clave secreta para JWT (en producción, usar vault o variable de entorno)
jwt.secret=MiSecretoUltraSeguroParaTests
jwt.expiration-ms=3600000
DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
model/User.java
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // hashed
private String roles; // CSV (ej: "USER,ADMIN")
// getters y setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRoles() { return roles; }
public void setRoles(String roles) { this.roles = roles; }
}
repository/UserRepository.java
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
Optional findByUsername(String username);
boolean existsByUsername(String username);
}
dto/RegisterRequest.java
package com.example.demo.dto;
public class RegisterRequest {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
dto/AuthRequest.java
package com.example.demo.dto;
public class AuthRequest {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
dto/AuthResponse.java
package com.example.demo.dto;
public class AuthResponse {
private String token;
public AuthResponse(String token) { this.token = token; }
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
}
service/UserService.java
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public User register(String username, String rawPassword) {
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException("Usuario ya existe");
}
User u = new User();
u.setUsername(username);
u.setPassword(passwordEncoder.encode(rawPassword));
u.setRoles("USER");
return userRepository.save(u);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
Collection authorities = Arrays.stream(user.getRoles().split(","))
.map(String::trim)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
}
security/JwtUtil.java
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();
Date exp = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(exp)
.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;
}
}
}
security/JwtFilter.java
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.beans.factory.annotation.Autowired;
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 {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
if (jwtUtil.validateToken(token)) {
username = jwtUtil.extractUsername(token);
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
}
security/SecurityConfig.java
package com.example.demo.security;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/h2-console/**").permitAll()
.anyRequest().authenticated()
);
// para H2 console
http.headers(headers -> headers.frameOptions(frame -> frame.disable()));
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
controller/AuthController.java
package com.example.demo.controller;
import com.example.demo.dto.AuthRequest;
import com.example.demo.dto.AuthResponse;
import com.example.demo.dto.RegisterRequest;
import com.example.demo.security.JwtUtil;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/register")
public ResponseEntity> register(@RequestBody RegisterRequest req) {
try {
userService.register(req.getUsername(), req.getPassword());
return ResponseEntity.ok("Usuario registrado");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PostMapping("/login")
public ResponseEntity> login(@RequestBody AuthRequest req) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword())
);
String token = jwtUtil.generateToken(req.getUsername());
return ResponseEntity.ok(new AuthResponse(token));
} catch (AuthenticationException e) {
return ResponseEntity.status(401).body("Credenciales inválidas");
}
}
@GetMapping("/ping")
public String ping() {
return "pong";
}
}
Por qué estas decisiones
- JWT: permite autenticación stateless, útil para APIs que escalan horizontalmente. No almacenas sesión en servidor.
- BCrypt: algoritmo probado para hashing de contraseñas, con salt y costo configurable.
- OncePerRequestFilter + SecurityContext: interceptas cada petición, validas el token y construyes la autenticación para Spring Security.
- H2: base en memoria para pruebas; en producción usa PostgreSQL/MySQL y guarda la clave JWT en un vault o variable de entorno.
Pruebas rápidas
- Arranca la app: mvn spring-boot:run
- Registrar: POST /auth/register con JSON {"username":"user","password":"pass"}
- Login: POST /auth/login con JSON {"username":"user","password":"pass"} → recibirás {"token":"..."}
- Acceder a endpoint protegido: GET /api/protected (crea uno en un controller nuevo) con header Authorization: Bearer <token>
Notas de seguridad y recomendaciones
- No pongas la clave JWT en el properties en producción; usa variables de entorno o servicios secretos.
- Configura tiempo de expiración adecuado y soporte de refresh tokens si necesitas sesiones largas.
- Valida y sanitiza inputs al registrar usuarios (ej. límites de longitud, validación de contraseñas).
Consejo avanzado: implementa rotación de claves y un mecanismo de revocación de tokens (por ejemplo, un token blacklist en Redis o usar tokens de corta vida con refresh tokens guardados en DB con revocación). También considera usar OAuth2 / OpenID Connect si necesitas integración con terceros o SSO.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación