Proyecto práctico: API de autenticación con JWT en Java (Spring Boot + PostgreSQL)
Construiremos una API REST mínima que implemente registro, login y protección de endpoints usando JWT. El objetivo es tener código completo, entendible y lista para extender: no usaré Lombok para que todo el código sea explícito.
Requisitos previos
- Java 11+
- Maven 3+
- PostgreSQL (o puedes cambiar a H2 para pruebas)
- Conocimientos básicos de Spring Boot, JPA y seguridad web
Estructura de carpetas
auth-project/
├─ pom.xml
└─ src/main/java/com/example/auth/
├─ Application.java
├─ config/SecurityConfig.java
├─ filter/JwtFilter.java
├─ model/User.java
├─ model/Role.java
├─ repository/UserRepository.java
├─ service/UserService.java
├─ util/JwtUtil.java
└─ web/AuthController.java
src/main/resources/
├─ application.properties
pom.xml (dependencias mínimas)
<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>auth-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
</parent>
<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>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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:postgresql://localhost:5432/authdb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# JWT
jwt.secret=ReplaceThisWithAStrongSecretKeyOfAtLeast32Chars
jwt.expiration=3600000 # 1 hour en ms
Archivo principal
package com.example.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Modelo de usuario y rol
package com.example.auth.model;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@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; // almacenado con BCrypt
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set roles = new HashSet<>();
public User() {}
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 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 Set getRoles() { return roles; }
public void setRoles(Set roles) { this.roles = roles; }
}
// Role.java
package com.example.auth.model;
public enum Role {
USER, ADMIN
}
Repositorio
package com.example.auth.repository;
import com.example.auth.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
Optional<User> findByUsername(String username);
}
Servicio de usuario
package com.example.auth.service;
import com.example.auth.model.Role;
import com.example.auth.model.User;
import com.example.auth.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.passwordEncoder = new BCryptPasswordEncoder();
}
public User register(String username, String rawPassword) {
if (userRepository.findByUsername(username).isPresent()) {
throw new IllegalArgumentException("Usuario ya existe");
}
User u = new User();
u.setUsername(username);
u.setPassword(passwordEncoder.encode(rawPassword));
u.setRoles(Collections.singleton(Role.USER));
return userRepository.save(u);
}
public User findByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
public boolean checkPassword(User user, String rawPassword) {
return passwordEncoder.matches(rawPassword, user.getPassword());
}
}
Utilidad para JWT
package com.example.auth.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expirationMillis;
public String generateToken(String username) {
Date now = new Date();
Date exp = new Date(now.getTime() + expirationMillis);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claimsResolver.apply(claims);
}
public boolean isTokenValid(String token, String username) {
final String tokenUser = extractUsername(token);
return (tokenUser.equals(username) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
Filtro JWT
package com.example.auth.filter;
import com.example.auth.service.UserService;
import com.example.auth.util.JwtUtil;
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.User;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserService userService;
public JwtFilter(JwtUtil jwtUtil, UserService userService) {
this.jwtUtil = jwtUtil;
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
try {
username = jwtUtil.extractUsername(token);
} catch (Exception e) {
// token inválido, seguimos sin autenticación
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
com.example.auth.model.User appUser = userService.findByUsername(username);
if (appUser != null && jwtUtil.isTokenValid(token, username)) {
UserDetails userDetails = User.withUsername(appUser.getUsername())
.password(appUser.getPassword())
.authorities(appUser.getRoles().stream().map(Enum::name).collect(Collectors.toList()).toArray(new String[0]))
.build();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
Configuración de seguridad
package com.example.auth.config;
import com.example.auth.filter.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.builders.AuthenticationManagerBuilder;
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.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 filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/register", "/auth/login").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Controlador de autenticación
package com.example.auth.web;
import com.example.auth.model.User;
import com.example.auth.service.UserService;
import com.example.auth.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.Map;
@RestController
@RequestMapping("/auth")
@Validated
public class AuthController {
private final UserService userService;
private final JwtUtil jwtUtil;
public AuthController(UserService userService, JwtUtil jwtUtil) {
this.userService = userService;
this.jwtUtil = jwtUtil;
}
static class AuthRequest {
@NotBlank
public String username;
@NotBlank
public String password;
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody AuthRequest req) {
try {
User u = userService.register(req.username, req.password);
return ResponseEntity.ok(Map.of("id", u.getId(), "username", u.getUsername()));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody AuthRequest req) {
User u = userService.findByUsername(req.username);
if (u == null || !userService.checkPassword(u, req.password)) {
return ResponseEntity.status(401).body(Map.of("error", "Credenciales inválidas"));
}
String token = jwtUtil.generateToken(u.getUsername());
return ResponseEntity.ok(Map.of("token", token));
}
@GetMapping("/me")
public ResponseEntity<?> me(org.springframework.security.core.Authentication authentication) {
return ResponseEntity.ok(Map.of("user", authentication.getName(), "authorities", authentication.getAuthorities()));
}
}
Por qué estas decisiones (explicado breve)
- JWT stateless: evita sesiones en servidor; ideal para microservicios o APIs REST.
- BCrypt: almacenamos hashes seguros en lugar de contraseñas planas.
- Filtro OncePerRequestFilter: valida el token en cada petición y construye Authentication para Spring Security.
- Roles como Enum y ElementCollection: simple y suficiente para permisos básicos sin tabla adicional.
- Secret en properties: en producción debes gestionarlo con un vault o variables de entorno, nunca en el repo.
Cómo probar
- Crea la base de datos: p. ej.
createdb authdby ajusta usuario/contraseña en properties. - Arranca:
mvn spring-boot:run. - Registrar: POST /auth/register con JSON {"username":"user","password":"pass"}.
- Login: POST /auth/login devuelve {"token":"..."}.
- Usar token: llamar GET /auth/me con header
Authorization: Bearer <token>.
Temas de seguridad y mejoras recomendadas
- No uses una clave JWT estática en el repo; usa un secreto fuerte gestionado externamente.
- Implementa refresh tokens y revocación (guardar tokens en DB o usar listas de bloqueo).
- Protege endpoints de administración y añade control de roles más fino con expresiones.
- Considera almacenamiento del token en cookie HttpOnly+Secure si tu cliente es navegador (reduce XSS).
Siguiente paso: añade refresh tokens y rotación, y sustituye la verificación de roles por una tabla separada si planificas permisos dinámicos o múltiples permisos por rol. También audita el manejo de excepciones para no filtrar detalles sensibles en respuestas de error.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación