FailedAttempts.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2025 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.web.lib.mvc.login;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.TemporalAmount;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.openspcoop2.utils.UtilsException;
import org.slf4j.Logger;
/**
* Bean per gestire i tentativi falliti di login
*
* @author Pintori Giuliano (pintori@link.it)
* @author $Author$
* @version $Rev$, $Date$
*
*/
public class FailedAttempts {
private ConcurrentMap<String, LoginAttempt> failedAttemptsMap;
private List<Duration> failedDelays;
public static FailedAttempts getInstance() {
// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED': l'istanza viene creata allo startup
synchronized (FailedAttempts.class) {
if (Holder.instance == null) {
throw new IllegalStateException("Instance not initialized. Call createInstance() first.");
}
return Holder.instance;
}
}
public static void createInstance(String loginRetryDelays) throws UtilsException {
// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED': l'istanza viene creata allo startup
synchronized (FailedAttempts.class) {
if (Holder.instance == null) {
Holder.instance = new FailedAttempts(loginRetryDelays);
} else {
throw new UtilsException("Instance already created");
}
}
}
private FailedAttempts(String loginRetryDelays) throws UtilsException {
this.failedAttemptsMap = new ConcurrentHashMap<>();
List<Duration> loginRertyDelays = this.getFailedAttemptDelay(loginRetryDelays);
this.failedDelays = new ArrayList<>(loginRertyDelays);
}
// Classe interna statica che inizializza l'istanza in modo thread-safe e lazy
private static class Holder {
private static FailedAttempts instance;
}
public LoginAttempt get(String username) {
return this.failedAttemptsMap.get(username);
}
public boolean bloccaUtente(Logger log, String username) {
LoginAttempt attempt = this.get(username);
if (attempt != null && attempt.getExpiring().isAfter(Instant.now())) {
log.error("Accesso fallito utente: {} utente bloccato causa troppi tentativi fino a: {}", username, attempt.getExpiring());
return true;
}
return false;
}
public List<Duration> getFailedAttemptDelay(String loginRetryDelays) throws UtilsException{
List<String> prop = List.of(loginRetryDelays.split(","));
List<Duration> parsedProp = new ArrayList<>();
Integer prev = 0;
for (String p : prop) {
Integer curr = Integer.parseInt(p.trim());
if (prev > curr)
throw new UtilsException("I tempi di attesa nel caso di credenziali errate devono essere incrementali");
parsedProp.add(Duration.ofSeconds(curr));
}
return parsedProp;
}
public void aggiungiTentativoFallitoUtente(Logger log, String username) {
this.failedAttemptsMap.compute(username, (key, value) -> {
int retries = ((value == null) ? 0 : value.getRetries()) + 1;
Instant expires = Instant.now().plus(computeUserLockAmount(retries));
log.error("utente {} password errata, tentativo: {}, utente sbloccato dopo: {}", username, retries, expires);
return new LoginAttempt(expires, retries);
});
}
private TemporalAmount computeUserLockAmount(int retries) {
return this.failedDelays.get(Math.min(retries - 1, this.failedDelays.size() - 1));
}
public void resetTentativiUtente(Logger log,String username) {
log.debug("utente {} password corrretta", username);
this.failedAttemptsMap.remove(username);
}
}