Tu código, tu control: aprende autenticación en Java. ¡Haz clic aquí!
Crearás una API REST en Spring Boot que permita:
- Registrar usuarios (con contraseña encriptada).
- Autenticar usuarios (login) y recibir un JWT.
- Proteger endpoints con el token JWT.
Usaremos:
- Spring Boot (Web, Security, Data JPA)
- BCrypt para encriptar contraseñas
- jjwt para tokens JWT
- H2 como base de datos en memoria para el ejemplo
1. Prerrequisitos
- Java 17+
- Maven
- Conocimientos básicos de Spring Boot
2. Dependencias pom.xml
(fragmento)
<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>
3. application.properties
(mínimo para demo)
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
spring.h2.console.enabled=true
# JWT (llave de ejemplo; en producción usa variable de entorno y algo más seguro)
jwt.secret=mi-secreto-muy-largo-y-seguro
jwt.expiration=3600000 # 1 hora en ms
4. Entidad User
package com.example.auth.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; // guardada en BCrypt
// opcional: rol, nombre, email...
private String role = "USER";
// getters y setters
}
5. Repositorio UserRepository
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<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
}
6. Servicio que implementa UserDetailsService
package com.example.auth.service;
import com.example.auth.model.User;
import com.example.auth.repository.UserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository repo;
public CustomUserDetailsService(UserRepository repo) {
this.repo = repo;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User u = repo.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
return new org.springframework.security.core.userdetails.User(
u.getUsername(),
u.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_" + u.getRole()))
);
}
}
7. Utilitario JWT (JwtUtil
)
package com.example.auth.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 JwtUtil {
private final Key key;
private final long expirationMs;
public JwtUtil(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration}") 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 ex) {
return false;
}
}
}
8. Filtro JWT (JwtAuthenticationFilter
)
package com.example.auth.security;
import com.example.auth.service.CustomUserDetailsService;
import com.example.auth.util.JwtUtil;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import org.springframework.util.StringUtils;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, CustomUserDetailsService uds) {
this.jwtUtil = jwtUtil;
this.userDetailsService = uds;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
String token = bearer.substring(7);
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
9. Configuración de seguridad (SecurityConfig
)
package com.example.auth.config;
import com.example.auth.security.JwtAuthenticationFilter;
import com.example.auth.service.CustomUserDetailsService;
import com.example.auth.util.JwtUtil;
import org.springframework.context.annotation.*;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final CustomUserDetailsService uds;
private final JwtUtil jwtUtil;
public SecurityConfig(CustomUserDetailsService uds, JwtUtil jwtUtil) {
this.uds = uds;
this.jwtUtil = jwtUtil;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
p.setUserDetailsService(uds);
p.setPasswordEncoder(passwordEncoder());
return p;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtUtil, uds);
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/auth/**", "/h2-console/**").permitAll()
.anyRequest().authenticated()
.and()
.authenticationProvider(authProvider());
// H2 console requires this in dev
http.headers().frameOptions().disable();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
10. Controlador de autenticación (AuthController
)
package com.example.auth.controller;
import com.example.auth.model.User;
import com.example.auth.repository.UserRepository;
import com.example.auth.util.JwtUtil;
import org.springframework.http.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final UserRepository repo;
private final BCryptPasswordEncoder encoder;
private final JwtUtil jwtUtil;
public AuthController(UserRepository repo, BCryptPasswordEncoder encoder, JwtUtil jwtUtil) {
this.repo = repo;
this.encoder = encoder;
this.jwtUtil = jwtUtil;
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");
if (repo.existsByUsername(username)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Usuario ya existe");
}
User u = new User();
u.setUsername(username);
u.setPassword(encoder.encode(password));
repo.save(u);
return ResponseEntity.ok("Usuario registrado");
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");
return repo.findByUsername(username).map(user -> {
if (encoder.matches(password, user.getPassword())) {
String token = jwtUtil.generateToken(username);
return ResponseEntity.ok(Map.of("token", token));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Credenciales inválidas");
}
}).orElseGet(() -> ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Usuario no encontrado"));
}
}
11. Ejemplo de endpoint protegido
@RestController
@RequestMapping("/api")
public class ProtectedController {
@GetMapping("/profile")
public ResponseEntity<?> profile(Authentication authentication){
return ResponseEntity.ok(Map.of("user", authentication.getName()));
}
}
Este endpoint
/api/profile
solo responderá si la petición incluyeAuthorization: Bearer <token>
.
12. Pruebas (curl)
- Registrar:
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"juan","password":"12345"}'
- Login:
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"juan","password":"12345"}'
Respuesta: {"token":"<JWT_AQUI>"}
- Llamar endpoint protegido:
curl http://localhost:8080/api/profile \
-H "Authorization: Bearer <JWT_AQUI>"
13. Buenas prácticas y consideraciones
- Nunca guardes
jwt.secret
en el repo: usa variables de entorno o un secret manager. - Establece expiraciones razonables y soporte de refresh tokens si lo necesitas.
- En entornos productivos, usa librerías y configuraciones que soporten revocación de tokens o almacena tokens inválidos si es necesario.
- Añade validación y manejo de errores centralizado para respuestas limpias.
- Protege rutas sensibles con roles/authorities (ej. ADMIN, USER).
- Registra accesos y utiliza HTTPS.
14. Extensiones posibles (walk-up ideas)
- Añadir refresh tokens.
- Autenticación con OAuth2 / OpenID Connect (Keycloak, Auth0).
- Verificación de email al registrarse.
- Integración con base de datos real (Postgres) y migraciones Flyway/Liquibase.
- Rate limiting en endpoints de autenticación.
15. Código completo y arranque
- Estructura recomendada:
src/main/java/com/example/auth/
model/User.java
repository/UserRepository.java
service/CustomUserDetailsService.java
util/JwtUtil.java
security/JwtAuthenticationFilter.java
config/SecurityConfig.java
controller/AuthController.java
controller/ProtectedController.java
AuthApplication.java
- Ejecuta con
mvn spring-boot:run
y prueba los endpoints.
Tu código, tu control: aprende autenticación en Java. ¡Haz clic aquí!