Einfache Login Absicherung mit Spring Security und Grails

von Andreas Scheidmeir
Login Security

In diesem Blogpost möchte ich die einfache Absicherung von Logins gegen Brute-Force-Angriffe mittels Spring Security und Grails demonstrieren. Die Umsetzung stellt keine komplette Absicherung dar und sollte durch weitere Maßnahmen ergänzt werden, ist aber ein guter erster Schritt in Richtung mehr Sicherheit. Weitergehende Ideen schneide ich am Ende des Posts an.

Schritt 1: AuthenticationFailureListener

Um fehlschlagende Authentifizierungsversuche zu zählen, kann man mittels ApplicationListener auf das AuthenticationFailureBadCredentialsEvent horchen. Dieses Event wird durch Spring Security ausgelöst, wenn der Login durch falschen Nutzernamen oder Passwort fehlschlägt und liefert eben diese versuchten Credentials, daneben aber auch den genauen Fehlergrund, SessionID oder RemoteAdress.

In diesem Beispiel sperren wir über den Benutzernamen, den man über event.authentication.principal  erhält. Dieser wird an den LoginAttemptService übergeben, welcher die fehlgeschlagenen Versuche zählt. Schauen wir uns diesen nun an.

class CustomAuthenticationFailureListener implements
        ApplicationListener<AuthenticationFailureBadCredentialsEvent>
{

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
        final String username = event.authentication.principal.toString()
        loginAttemptService.loginFailed(username)
    }
}

Schritt 2: LoginAttemptService

Für das Zählen der fehlgeschlagenen Versuche pro Nutzername ist der LoginAttemptService zuständig. Hierzu wird ein Key-Value Cache angelegt, der seine Einträge für 30 Minuten vorhält. Bei jedem Aufruf der loginFailedMethode wird geprüft, ob der Benutzer als Key schon vorhanden ist und weiter hochgezählt oder neu angelegt. Im Fall, dass die maximale Anzahl von fehlgeschlagenen Versuchen erreicht ist, wird dies im Beispiel direkt noch geloggt. Zusätzlich stellt der Service mit isBlocked eine Methode bereit, über die geprüft werden kann, ob ein gegebener Benutzername temporär blockiert ist.

class LoginAttemptService {

    private final MAX_LOGIN_ATTEMPTS = 5
    private LoadingCache<String, Integer> attemptsCache

    LoginAttemptService() {
        super();
        attemptsCache = Caffeine.newBuilder().
                expireAfterWrite(30, TimeUnit.MINUTES).
                build(new CacheLoader<String, Integer>() {
            Integer load(String key) {
                return 0
            }
        })
    }

    void loginFailed(String key) {
        if (!key) return
        int attempts
        try {
            attempts = attemptsCache.get(key)
        } catch (ExecutionException ignored) {
            attempts = 0
        }
        attempts++;
        attemptsCache.put(key, attempts)
        if (attempts  >= MAX_LOGIN_ATTEMPTS) {
            FailedLoginLogger.logBlockedLogin(key)
        }
    }

    boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_LOGIN_ATTEMPTS
        } catch (Exception ignored) {
            return false
        }
    }
}

Schritt 3: PreAuthenticationChecks

Nun da wir prüfen können, ob ein Nutzer nach 5 Fehlschlägen geblockt ist, müssen weitere Login-Versuche abgewiesen werden. Wenn man den UserDetailsService selbst implementiert, kann die Überprüfung dort stattfinden. Wer dies nicht tut, so wie in unserem Beispiel, kann die UserDetailsChecker nutzen. Diese Checker werden vom UserDetailsService aufgerufen, um zusätzliche Überprüfungen abzuhandeln. In der check-Methode steht das angefragte User-Objekt zur Verfügung, über dessen Nutzernamen wir einfach gegen den LoginAttemptService prüfen können. Falls ein temporärer Block besteht, wird der Login Vorgang über das Werfen der Exception abgebrochen. Anstelle der generischen AuthenticationException kann natürlich eine Eigene erstellt und geworfen werden.

class LoginAttemptPreAuthenticationChecks implements UserDetailsChecker {
    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    void check(UserDetails user) {
        if (loginAttemptService.isBlocked(user.username)) {
            throw new AuthenticationException()
        }
    }
}

Schritt 4: Registrierung

Damit CustomAuthenticationFailureListener und LoginAttemptPreAuthenticationChecks angezogen werden, bedarf es noch einer Registrierung in der resources.groovy:

beans = {

    …
    preAuthenticationChecks(LoginAttemptPreAuthenticationChecks)
    customAuthenticationFailureListener(CustomAuthenticationFailureListener)
     …
}

Schritt 5: Feedback

Zu guter Letzt fehlt noch das Feedback. Nutzer müssen verstehen, wenn und warum der Login gesperrt wird. Dazu kann man im Exception Handling auf die AuthenticationException (oder eine CustomException) prüfen und entsprechend eine spezielle Fehlerseite ausgeben, die angibt, warum und wie lange der Zugang gesperrt ist. Damit ist die Umsetzung abgeschlossen.

Weitere Schritte

Anstatt über den Login User zu sperren kann natürlich auch die IP aus dem Request geprüft werden. Dies verhindert zum Beispiel, dass ein Angreifer mehrere Nutzerprofile blockieren kann. Welche Methode man wählt, hängt vom individuellen Einsatzzweck ab.

Darüber hinaus kann dieser Ansatz weiter ausgebaut werden, in dem CAPTCHAS eingebaut werden, um Bots auszuschließen, oder eine Zwei-Faktor Authentifizierung ergänzt wird. Wenn die Servicekapazität für das Entsperren von Accounts vorliegt, kann auch ein progressiver Lock Out Sinn machen. Dabei wird die Wartezeit nach fehlerhaften Login Versuchen nach und nach erhöht, bis schließlich der Zugang, nach dem Erreichen einer definierten Anzahl von Fehlschlägen, komplett gesperrt wird. Diese Ansätze schließen sich gegenseitig nicht aus.

Fazit

Dieser Blogpost zeigt eine einfache und schnelle Lösung auf, um mehr Sicherheit gegenüber Brute-Force Attacken zu gewährleisten, die keinen Mehraufwand im Betrieb (wie dem Entsperren von Accounts) nach sich zieht.

Dabei ist zu beachten: Dies ist keine umfassende Absicherung gegen Angriffe. Wenn z.B. der Loginserver über ein Cluster repliziert ist, kann auf jedem dieser Instanzen die Anzahl der Versuche ausgereizt werden. Der Ansatz ist lediglich ein Puzzlestück zur Absicherung von Anwendungen. Je nach Anwendungsfall sollten weitere Schritte in Betracht gezogen werden, wie zum Beispiel die bereits genannten Alternativen und/oder ein konsequentes Monitoring der Serveranfragen, um Anomalien zu erkennen und ggf. direkt auf Angriffe reagieren zu können.

Zurück

© 2006-2024 exensio GmbH
Einstellungen gespeichert
Datenschutzeinstellungen

Wir nutzen Cookies auf unserer Website. Einige von ihnen sind essenziell, während andere uns helfen, diese Website und Ihre Erfahrung zu verbessern.

Sie können Ihre Einwilligung jederzeit ändern oder widerrufen, indem Sie auf den Link in der Datenschutzerklärung klicken.

Zu den gesetzlichen Rechenschaftspflichten gehört die Einwilligung (Opt-In) zu protokollieren und archivieren. Aus diesem Grund wird Ihre Opt-In Entscheidung in eine LOG-Datei geschrieben. In dieser Datei werden folgende Daten gespeichert:

 

  • IP-Adresse des Besuchers
  • Vom Besucher gewählte Datenschutzeinstellung (Privacy Level)
  • Datum und Zeit des Speicherns
  • Domain
You are using an outdated browser. The website may not be displayed correctly. Close