Verhindern Sie Brute-Force-Authentifizierungsversuche mit der Spring-Sicherheit

Verhindern Sie Brute-Force-Authentifizierungsversuche mit Spring Security

1. Überblick

In diesem kurzen Tutorial implementieren wir eine Basislösung fürpreventing brute force authentication attempts mit Spring Security.

Einfach ausgedrückt: Wir protokollieren die Anzahl der fehlgeschlagenen Versuche, die von einer einzelnen IP-Adresse ausgehen. Wenn diese bestimmte IP-Adresse eine festgelegte Anzahl von Anforderungen überschreitet, wird sie für 24 Stunden blockiert.

Weitere Lektüre:

Einführung in die Spring-Methodensicherheit

Eine Anleitung zur Sicherheit auf Methodenebene mit dem Spring Security Framework.

Read more

Ein benutzerdefinierter Filter in der Spring-Sicherheitsfilterkette

Eine Kurzanleitung mit Schritten zum Hinzufügen eines benutzerdefinierten Filters im Spring Security-Kontext.

Read more

Federsicherheit 5 für reaktive Anwendungen

Ein schnelles und praktisches Beispiel für die Funktionen des Spring Security 5-Frameworks zur Sicherung reaktiver Anwendungen.

Read more

2. EinAuthenticationFailureEventListener

Beginnen wir mit der Definition vonAuthenticationFailureEventListener, umAuthenticationFailureBadCredentialsEvent Ereignisse abzuhören und uns über einen Authentifizierungsfehler zu informieren:

@Component
public class AuthenticationFailureListener
  implements ApplicationListener {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails)
          e.getAuthentication().getDetails();

        loginAttemptService.loginFailed(auth.getRemoteAddress());
    }
}

Beachten Sie, dass wir bei fehlgeschlagener Authentifizierung dieLoginAttemptService über die IP-Adresse informieren, von der der erfolglose Versuch stammt.

3. EinAuthenticationSuccessEventListener

Definieren wir auch einAuthenticationSuccessEventListener - das aufAuthenticationSuccessEvent Ereignisse wartet und uns über eine erfolgreiche Authentifizierung benachrichtigt:

@Component
public class AuthenticationSuccessEventListener
  implements ApplicationListener {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationSuccessEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails)
          e.getAuthentication().getDetails();

        loginAttemptService.loginSucceeded(auth.getRemoteAddress());
    }
}

Beachten Sie, wie - ähnlich wie beim Fehlerlistener - dieLoginAttemptService der IP-Adresse benachrichtigt werden, von der die Authentifizierungsanforderung stammt.

4. DieLoginAttemptService

Lassen Sie uns nun die Implementierung vonLoginAttemptServicediskutieren. Einfach ausgedrückt: Wir behalten die Anzahl der falschen Versuche pro IP-Adresse 24 Stunden lang bei:

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader() {
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void loginFailed(String key) {
        int attempts = 0;
        try {
            attempts = attemptsCache.get(key);
        } catch (ExecutionException e) {
            attempts = 0;
        }
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_ATTEMPT;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

Beachten Sie, wiean unsuccessful authentication attempt increases the number of attempts for that IP und die erfolgreiche Authentifizierung diesen Zähler zurücksetzen.

Ab diesem Punkt ist es einfach eine Frage vonchecking the counter when we authenticate.

5. DieUserDetailsService

Fügen wir nun die zusätzliche Prüfung in der Implementierung unserer benutzerdefiniertenUserDetailsServicehinzu. Wenn wirUserDetails laden,we first need to check if this IP address is blocked:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Autowired
    private HttpServletRequest request;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        String ip = getClientIP();
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("blocked");
        }

        try {
            User user = userRepository.findByEmail(email);
            if (user == null) {
                return new org.springframework.security.core.userdetails.User(
                  " ", " ", true, true, true, true,
                  getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
            }

            return new org.springframework.security.core.userdetails.User(
              user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true,
              getAuthorities(user.getRoles()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Und hier ist die Methode vongetClientIP():

private String getClientIP() {
    String xfHeader = request.getHeader("X-Forwarded-For");
    if (xfHeader == null){
        return request.getRemoteAddr();
    }
    return xfHeader.split(",")[0];
}

Beachten Sie, dass wir eine zusätzliche Logik füridentify the original IP address of the Client haben. In den meisten Fällen ist dies nicht erforderlich, in einigen Netzwerkszenarien jedoch.

In diesen seltenen Szenarien verwenden wir den HeaderX-Forwarded-For, um zur ursprünglichen IP zu gelangen. Hier ist die Syntax für diesen Header:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Beachten Sie auch eine andere super interessante Fähigkeit, die Spring hat -we need the HTTP request, so we’re simply wiring it in.

Das ist cool. Wir müssen unserenweb.xmleinen schnellen Listener hinzufügen, damit dies funktioniert, und das macht die Sache viel einfacher.


    
        org.springframework.web.context.request.RequestContextListener
    

Das war's auch schon - wir haben diese neuenRequestContextListenerin unserenweb.xmldefiniert, um vonUserDetailsServiceauf die Anfrage zugreifen zu können.

6. Ändern SieAuthenticationFailureHandler

Zum Schluss ändern wir unsereCustomAuthenticationFailureHandler, um unsere neue Fehlermeldung anzupassen.

Wir behandeln die Situation, in der der Benutzer tatsächlich 24 Stunden lang blockiert wird - und wir informieren den Benutzer, dass seine IP blockiert ist, weil er die maximal zulässigen falschen Authentifizierungsversuche überschritten hat:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Override
    public void onAuthenticationFailure(...) {
        ...

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);
        if (exception.getMessage().equalsIgnoreCase("blocked")) {
            errorMessage = messages.getMessage("auth.message.blocked", null, locale);
        }

        ...
    }
}

7. Fazit

Es ist wichtig zu verstehen, dass dies ein guter erster Schritt indealing with brute-force password attempts ist, aber auch, dass Verbesserungspotenzial besteht. Eine Brute-Force-Präventionsstrategie für die Produktion kann mehr als nur Elemente umfassen, die ein IP blockieren kann.

Diefull implementation dieses Tutorials finden Sie inthe github project - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.