API REST con Spring Boot + JWT + PostgreSQL — proyecto práctico
En este proyecto construirás una API REST minimalista con Spring Boot que implementa autenticación basada en JWT y persistencia en PostgreSQL. Incluyo los archivos principales, la configuración y explicaciones de por qué se hizo así.
Requisitos previos
- Java 17+
- Maven
- PostgreSQL (una base de datos accesible durante el desarrollo)
- Conocimientos básicos de Spring Boot y seguridad
Estructura de carpetas (esencial)
demo
├─ pom.xml
└─ src
└─ main
├─ java
│ └─ com.example.demo
│ ├─ DemoApplication.java
│ ├─ config
│ │ ├─ SecurityConfig.java
│ │ └─ JwtFilter.java
│ ├─ controller
│ │ └─ AuthController.java
│ ├─ dto
│ │ ├─ AuthRequest.java
│ │ ├─ AuthResponse.java
│ │ └─ RegisterRequest.java
│ ├─ model
│ │ └─ User.java
│ ├─ repository
│ │ └─ UserRepository.java
│ ├─ service
│ │ └─ UserService.java
│ └─ util
│ └─ JwtUtils.java
└─ resources
└─ application.properties
pom.xml (dependencias clave)
<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>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<properties>
<java.version>17</java.version>
<spring.boot.version>3.1.0</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>org.postgresql</groupId>
<artifactId>postgresql</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:postgresql://localhost:5432/demo
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# JWT
jwt.secret=change_this_secret_to_a_long_random_value
jwt.expiration-ms=3600000
Archivos Java principales
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;
private String role = "USER"; // simple role
// 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 getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
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<User> findByUsername(String username);
}
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; }
}
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; }
}
service/UserService.java
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
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.Collections;
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public User register(String username, String rawPassword) {
User u = new User();
u.setUsername(username);
u.setPassword(passwordEncoder.encode(rawPassword));
u.setRole("USER");
return userRepository.save(u);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(user.getRole())
.build();
}
}
util/JwtUtils.java
package com.example.demo.util;
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 JwtUtils {
private final Key key;
private final long expirationMs;
public JwtUtils(@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)
.compact();
}
public String getUsernameFromToken(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;
}
}
}
config/JwtFilter.java
package com.example.demo.config;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserService userService;
public JwtFilter(JwtUtils jwtUtils, UserService userService) {
this.jwtUtils = jwtUtils;
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
config/SecurityConfig.java
package com.example.demo.config;
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.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@Configuration
@EnableMethodSecurity
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()
.authorizeHttpRequests()
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
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.model.User;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
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 {
private final UserService userService;
private final JwtUtils jwtUtils;
private final AuthenticationManager authenticationManager;
public AuthController(UserService userService, JwtUtils jwtUtils, AuthenticationManager authenticationManager) {
this.userService = userService;
this.jwtUtils = jwtUtils;
this.authenticationManager = authenticationManager;
}
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody RegisterRequest req) {
User u = userService.register(req.getUsername(), req.getPassword());
return ResponseEntity.ok("User created: " + u.getUsername());
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthRequest req) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword())
);
String token = jwtUtils.generateToken(req.getUsername());
return ResponseEntity.ok(new AuthResponse(token));
} catch (AuthenticationException ex) {
return ResponseEntity.status(401).body("Invalid credentials");
}
}
}
Por qué estas decisiones
- BCrypt para
PasswordEncoder: fuerte y estándar para hashing de contraseñas. - JWT stateless: evita sesiones en el servidor, escalable en microservicios.
- Filtro OncePerRequestFilter: centraliza la validación del token y la autenticación del SecurityContext.
- DTOs (AuthRequest/AuthResponse): separación clara entre API y entidad persistente.
- jjwt: librería simple para generar y validar tokens; usa claves HMAC seguras.
- spring.jpa.hibernate.ddl-auto=update en desarrollo; para producción usa migraciones (Flyway/Liquibase).
Pruebas rápidas
- Arranca PostgreSQL y ajusta
application.properties. - mvn spring-boot:run
- POST /auth/register con JSON {"username":"user1","password":"pass"}
- POST /auth/login con JSON {"username":"user1","password":"pass"} → obtendrás token
- Accede a un endpoint protegido añadiendo cabecera Authorization: Bearer <token>
Consideraciones y siguientes pasos
- Usa una clave JWT larga y segura (no pongas la real en properties; carga desde vault/variables de entorno).
- Implementa refresco de tokens (refresh tokens) si necesitas sesiones largas.
- Audita permisos: ahora sólo hay un rol simple. Para APIs reales, modela permisos más finos y comprobaciones en métodos o endpoints.
- Migra a Flyway/Liquibase para control de esquema en producción y usa tests de integración contra una DB en memoria o contenedor.
Tip avanzado: protege aún más las rutas críticas usando claims custom en el token (por ejemplo, una lista de permisos) y valida esos claims en un MethodSecurityExpressionHandler o en un filtro dedicado. Advertencia de seguridad: nunca confíes en datos del token sin verificar firma y expiración; además, evita poner datos sensibles en el payload del JWT.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación