
flowchart LR
A["POST /login"] --> B[AuthenticationManager]
B --> C[UserDetailsService]
C --> D[BCrypt verifica password]
D -- ok --> E[Authentication]
E --> F[Generar JWT HS256]
F --> G[Cliente recibe token]
G --> H[Authorization Bearer]
H --> I[JwtFilter valida]
I --> J[Endpoint protegido]
Autenticar empleados con un método de login desde un controlador REST en Spring Boot. La autenticación se basa en emitir un token JWT firmado con HMAC-SHA256 que el cliente enviará en cada petición posterior.
Este flujo es el habitual en una API REST corporativa: el empleado introduce sus credenciales desde el portal interno de Consultora Técnica SL, el backend valida contra la tabla empleados y devuelve un JWT con los claims de rol y departamento que usará el resto del sistema para autorizar.
Dependencias y modelo de datos
La dependencia de referencia es JJWT 0.12+, con la API moderna Jwts.builder() / Jwts.parser() que reemplaza los métodos obsoletos parseClaimsJws de versiones 0.11.x.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
Crear método login en controlador
Primero definimos un record LoginRequest para recibir las credenciales del empleado:
public record LoginRequest(
@NotBlank @Email String email,
@NotBlank String password
) {
}
Y un record TokenResponse para la respuesta:
public record TokenResponse(
String accessToken,
long expiresInSeconds
) {
}
Ahora el método login en el controlador REST. Delegamos la autenticación en el AuthenticationManager de Spring Security: él llamará al UserDetailsService, verificará la contraseña con el PasswordEncoder configurado y lanzará BadCredentialsException si algo no cuadra. Esto es más seguro y limpio que comparar manualmente con passwordEncoder.matches(), porque centraliza la política en un solo sitio y protege frente a ataques de timing.
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
public AuthController(AuthenticationManager authenticationManager,
JwtService jwtService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@PostMapping("/login")
public TokenResponse login(@RequestBody @Valid LoginRequest login) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(login.email(), login.password())
);
Empleado empleado = (Empleado) authentication.getPrincipal();
String token = jwtService.generarToken(empleado);
return new TokenResponse(token, Duration.ofHours(8).toSeconds());
}
}
El JwtService encapsula la construcción del token. Nunca hardcodeamos la clave en código fuente: la inyectamos desde una propiedad respaldada por variable de entorno o por un gestor de secretos (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault):
@Service
public class JwtService {
private final SecretKey secretKey;
private final Duration tokenTtl;
private final String issuer;
public JwtService(
@Value("${app.jwt.secret}") String secretBase64,
@Value("${app.jwt.ttl:PT8H}") Duration tokenTtl,
@Value("${app.jwt.issuer:consultora-tecnica-sl}") String issuer) {
byte[] keyBytes = Base64.getDecoder().decode(secretBase64);
if (keyBytes.length < 32) {
throw new IllegalStateException(
"app.jwt.secret debe tener al menos 256 bits (32 bytes) tras decodificar Base64");
}
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
this.tokenTtl = tokenTtl;
this.issuer = issuer;
}
public String generarToken(Empleado empleado) {
Instant ahora = Instant.now();
Instant expiracion = ahora.plus(tokenTtl);
return Jwts.builder()
.issuer(issuer)
.subject(String.valueOf(empleado.getId()))
.issuedAt(Date.from(ahora))
.expiration(Date.from(expiracion))
.claim("email", empleado.getEmail())
.claim("rol", empleado.getRol().name())
.claim("departamento", empleado.getDepartamento())
.signWith(secretKey, Jwts.SIG.HS256)
.compact();
}
}
Puntos clave del método:
- El
AuthenticationManagercomprueba las credenciales y lanzaBadCredentialsExceptionsi fallan. Un@RestControllerAdvicela traduce a401 Unauthorizedsin filtrar si el email existe o no. Jwts.SIG.HS256es la constante oficial de la API moderna de JJWT 0.12+.tokenTtlse parsea comoDurationISO-8601 (PT8H= 8 horas). Para un portal interno 8 horas suele cubrir una jornada; para un dashboard financiero conviene bajarlo a 15 minutos combinado con refresh tokens.- La clave secreta se valida en el constructor: si tiene menos de 256 bits, la aplicación falla al arrancar (fail-fast). Mucho mejor que un error en runtime cuando el primer empleado intenta entrar.
Configuración en application.yml
app:
jwt:
# Clave inyectada desde APP_JWT_SECRET (variable de entorno), Vault o AWS Secrets Manager.
# Generación: openssl rand -base64 32
secret: ${APP_JWT_SECRET}
ttl: PT8H
issuer: consultora-tecnica-sl
Nunca comitees valores reales; usa application-local.yml en .gitignore durante desarrollo.
Handler de errores coherente
Para que el cliente reciba un 401 limpio sin detalles que faciliten enumeración de cuentas:
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Map<String, String> credencialesInvalidas() {
return Map.of("error", "credenciales_invalidas");
}
}
Verificar login desde Postman
Enviamos el email y password de un empleado existente:

Ejemplo del token generado:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjb25zdWx0b3JhLXRlY25pY2Etc2wiLCJzdWIi...
Se puede decodificar en jwt.io para inspeccionar los claims durante desarrollo:

Nota: jwt.io solo permite verificar la firma si pegas la clave. En producción nunca pegues la clave real en herramientas web; usa un script local con JJWT o
jose-util.
Verificar login desde frontend
Desde un frontend Angular basta un HttpClient.post al endpoint:
login(email: string, password: string): Observable<TokenResponse> {
return this.http.post<TokenResponse>('/api/auth/login', { email, password });
}
El token recibido se almacena en memoria (o en sessionStorage si es aceptable para el modelo de amenazas del cliente) y se envía en la cabecera Authorization: Bearer <token> mediante un HttpInterceptor.

Para escenarios más estrictos (datos financieros, auditoría FUNDAE), conviene combinar este login con refresh tokens de corta duración y con una lista de revocación basada en jti. Esos patrones se detallan en lecciones posteriores del módulo.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Autenticar empleados delegando en AuthenticationManager. Firmar un JWT con HMAC-SHA256 usando la API moderna de JJWT 0.12+. Inyectar la clave secreta desde variable de entorno o gestor de secretos. Incluir claims corporativos (rol, departamento) en el token.