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.
Ein benutzerdefinierter Filter in der Spring-Sicherheitsfilterkette
Eine Kurzanleitung mit Schritten zum Hinzufügen eines benutzerdefinierten Filters im Spring Security-Kontext.
Federsicherheit 5 für reaktive Anwendungen
Ein schnelles und praktisches Beispiel für die Funktionen des Spring Security 5-Frameworks zur Sicherung reaktiver Anwendungen.
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.