Login de usuarios en API REST

Avanzado
Spring Security
Spring Security
Actualizado: 04/05/2026

Diagrama: tutorial-spring-boot-login

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 AuthenticationManager comprueba las credenciales y lanza BadCredentialsException si fallan. Un @RestControllerAdvice la traduce a 401 Unauthorized sin filtrar si el email existe o no.
  • Jwts.SIG.HS256 es la constante oficial de la API moderna de JJWT 0.12+.
  • tokenTtl se parsea como Duration ISO-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 - Autor del tutorial

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.