Si ya tienes una API REST con Spring Boot funcionando, el siguiente paso es protegerla. Y la forma más usada para hacerlo en aplicaciones modernas es JWT — JSON Web Token. En este artículo te explico qué es, cómo funciona y cómo implementar autenticación con JWT en Spring Boot desde cero.
Si aún no tienes tu API montada, te recomiendo leer primero el artículo sobre qué es Spring Boot y cómo conectar Angular con Spring Boot antes de continuar.
¿Qué es JWT?
JWT (JSON Web Token) es un estándar abierto para transmitir información de forma segura entre dos partes como un objeto JSON firmado digitalmente. Se usa principalmente para autenticación: el usuario inicia sesión, el servidor genera un token y el cliente lo envía en cada petición para demostrar que está autenticado.
Un token JWT tiene tres partes separadas por puntos:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3VhcmlvQGVqZW1wbG8uY29tIn0.abc123
- Header: algoritmo de firma usado (HS256, RS256…)
- Payload: datos del usuario (email, rol, fecha de expiración…)
- Signature: firma digital que garantiza que el token no ha sido manipulado
La clave de JWT es que el servidor no necesita guardar sesiones. Simplemente verifica la firma del token en cada petición y extrae los datos del usuario del propio token. Esto lo hace ideal para APIs REST y arquitecturas sin estado.
Cómo funciona la autenticación JWT en Spring Boot
El flujo de autenticación con JWT es siempre el mismo:
- El usuario envía su email y contraseña al endpoint
/auth/login - Spring Boot verifica las credenciales contra la base de datos
- Si son correctas, genera un token JWT firmado y lo devuelve al cliente
- El cliente guarda el token (en memoria o localStorage) y lo envía en cada petición en la cabecera
Authorization: Bearer {token} - Spring Boot intercepta cada petición, verifica el token y permite o deniega el acceso
Dependencias necesarias para JWT en Spring Boot
Añade estas dependencias en tu pom.xml. Usaremos la librería jjwt, la más popular para trabajar con JWT en Java:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
Estructura del proyecto
Antes de empezar a escribir código, la estructura que vamos a montar es la siguiente:
src/main/java/com/tuapp/
├── auth/
│ ├── AuthController.java ← endpoint de login y registro
│ ├── AuthService.java ← lógica de autenticación
│ └── AuthRequest.java ← DTO con email y password
├── config/
│ ├── SecurityConfig.java ← configuración de Spring Security
│ ├── JwtFilter.java ← filtro que intercepta cada petición
│ └── JwtService.java ← generación y validación de tokens
└── usuario/
├── Usuario.java ← entidad de usuario
└── UsuarioRepository.java ← repositorio JPA
Paso 1: la entidad Usuario
El usuario necesita implementar UserDetails para integrarse con Spring Security:
@Entity
@Table(name = "usuarios")
public class Usuario implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
@Column(unique = true)
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Rol rol;
// Métodos de UserDetails
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(rol.name()));
}
@Override
public String getUsername() { return email; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
// Getters y setters
public Long getId() { return id; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public void setEmail(String email) { this.email = email; }
public void setPassword(String password) { this.password = password; }
public void setRol(Rol rol) { this.rol = rol; }
}
// Enum de roles
public enum Rol {
USER, ADMIN
}
Paso 2: el servicio JWT
Esta clase se encarga de generar y validar los tokens. Guarda la clave secreta en application.properties, nunca en el código:
# application.properties
jwt.secret=miClaveSecretaSuperSeguraDeAlMenos256BitsParaHS256
jwt.expiration=86400000
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expiration;
public String generarToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
public String extraerEmail(String token) {
return extraerClaim(token, Claims::getSubject);
}
public boolean esValido(String token, UserDetails userDetails) {
String email = extraerEmail(token);
return email.equals(userDetails.getUsername()) && !estaExpirado(token);
}
private boolean estaExpirado(String token) {
return extraerClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extraerClaim(String token, Function<Claims, T> resolver) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
Paso 3: el filtro JWT
Este filtro intercepta cada petición HTTP, extrae el token de la cabecera y valida al usuario:
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private 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 ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String email = jwtService.extraerEmail(token);
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtService.esValido(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Paso 4: configuración de Spring Security
Aquí defines qué endpoints son públicos y cuáles requieren autenticación:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Paso 5: el controlador de autenticación
El endpoint de login que el frontend llamará para obtener el token:
// DTO de petición
public class AuthRequest {
private String email;
private String password;
// Getters y setters
}
// DTO de respuesta
public class AuthResponse {
private String token;
public AuthResponse(String token) { this.token = token; }
public String getToken() { return token; }
}
// Controlador
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.login(request));
}
@PostMapping("/registro")
public ResponseEntity<AuthResponse> registro(@RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.registro(request));
}
}
// Servicio
@Service
public class AuthService {
@Autowired
private UsuarioRepository repo;
@Autowired
private JwtService jwtService;
@Autowired
private AuthenticationManager authManager;
@Autowired
private PasswordEncoder passwordEncoder;
public AuthResponse login(AuthRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
);
Usuario usuario = repo.findByEmail(request.getEmail()).orElseThrow();
String token = jwtService.generarToken(usuario);
return new AuthResponse(token);
}
public AuthResponse registro(AuthRequest request) {
Usuario usuario = new Usuario();
usuario.setEmail(request.getEmail());
usuario.setPassword(passwordEncoder.encode(request.getPassword()));
usuario.setRol(Rol.USER);
repo.save(usuario);
String token = jwtService.generarToken(usuario);
return new AuthResponse(token);
}
}
Probarlo con Postman
Antes de conectarlo con Angular, prueba que la API funciona correctamente con Postman:
1. Registro:
POST http://localhost:8080/api/auth/registro
Content-Type: application/json
{
"email": "usuario@ejemplo.com",
"password": "miPassword123"
}
Respuesta esperada:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3VhcmlvQGVqZW1wbG8uY29tIn0..."
}
2. Login:
POST http://localhost:8080/api/auth/login
Content-Type: application/json
{
"email": "usuario@ejemplo.com",
"password": "miPassword123"
}
3. Petición protegida:
GET http://localhost:8080/api/productos
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Errores más comunes con JWT en Spring Boot
❌ 403 Forbidden en todos los endpoints
Causa: Spring Security está bloqueando todo por defecto. Solución: revisa que /api/auth/** está marcado como permitAll() en SecurityConfig.
❌ JwtException: JWT signature does not match
Causa: la clave secreta en application.properties no está en Base64 o es demasiado corta. Solución: genera una clave Base64 de al menos 256 bits.
❌ 401 Unauthorized aunque el token es correcto
Causa: el token ha expirado o el filtro JWT no está añadido antes de UsernamePasswordAuthenticationFilter. Solución: revisa el orden del filtro en SecurityConfig y aumenta el tiempo de expiración durante el desarrollo.
Resumen: autenticación JWT en Spring Boot
Implementar JWT en Spring Boot requiere cinco piezas: la entidad Usuario que implementa UserDetails, el JwtService que genera y valida tokens, el JwtFilter que intercepta cada petición, la configuración de Spring Security y el controlador de autenticación con los endpoints de login y registro. Con esto tienes una API REST completamente protegida lista para conectar con Angular.
En el próximo artículo veremos cómo gestionar el token JWT desde Angular — guardarlo, enviarlo en cada petición con un interceptor y manejar la expiración automáticamente.