Empêcher les tentatives d'authentification par force brute avec Spring Security
1. Vue d'ensemble
Dans ce rapide didacticiel, nous allons mettre en œuvre une solution de base pourpreventing brute force authentication attempts à l'aide de Spring Security.
En termes simples, nous conserverons un enregistrement du nombre de tentatives infructueuses provenant d'une seule adresse IP. Si cette IP particulière dépasse un nombre défini de demandes, elle sera bloquée pendant 24 heures.
Lectures complémentaires:
Introduction à la sécurité de la méthode Spring
Guide sur la sécurité au niveau des méthodes à l'aide de la structure Spring Security.
Un filtre personnalisé dans la chaîne de filtres de sécurité Spring
Un guide rapide expliquant la marche à suivre pour ajouter un filtre personnalisé dans le contexte Spring Security.
Spring Security 5 pour applications réactives
Un exemple rapide et pratique des fonctionnalités du framework Spring Security 5 pour sécuriser les applications réactives.
2. UnAuthenticationFailureEventListener
Commençons par définir unAuthenticationFailureEventListener - pour écouter les événements deAuthenticationFailureBadCredentialsEvent et nous avertir d'un échec d'authentification:
@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());
}
}
Notez comment, lorsque l'authentification échoue, nous informons lesLoginAttemptService de l'adresse IP d'où provient la tentative infructueuse.
3. UnAuthenticationSuccessEventListener
Définissons également unAuthenticationSuccessEventListener - qui écoute les événements deAuthenticationSuccessEvent et nous informe d'une authentification réussie:
@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());
}
}
Notez comment - à l'instar de l'écouteur d'échec, nous notifions lesLoginAttemptService de l'adresse IP d'où provient la demande d'authentification.
4. LesLoginAttemptService
Maintenant - parlons de notre implémentation deLoginAttemptService; en termes simples - nous conservons le nombre de tentatives erronées par adresse IP pendant 24 heures:
@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;
}
}
}
Remarquez commentan unsuccessful authentication attempt increases the number of attempts for that IP, et l'authentification réussie réinitialise ce compteur.
À partir de là, c'est simplement une question dechecking the counter when we authenticate.
5. LesUserDetailsService
Maintenant, ajoutons la vérification supplémentaire dans notre implémentation personnalisée deUserDetailsService; quand on charge lesUserDetails,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);
}
}
}
Et voici la méthodegetClientIP():
private String getClientIP() {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null){
return request.getRemoteAddr();
}
return xfHeader.split(",")[0];
}
Notez que nous avons une logique supplémentaire pouridentify the original IP address of the Client. Dans la plupart des cas, cela ne sera pas nécessaire, mais dans certains scénarios de réseau, c'est le cas.
Pour ces rares scénarios, nous utilisons l'en-têteX-Forwarded-For pour accéder à l'adresse IP d'origine; voici la syntaxe de cet en-tête:
X-Forwarded-For: clientIpAddress, proxy1, proxy2
Notez également une autre capacité super intéressante que Spring a -we need the HTTP request, so we’re simply wiring it in.
Maintenant, c’est cool. Nous devrons ajouter un auditeur rapide à nosweb.xml pour que cela fonctionne, et cela rend les choses beaucoup plus faciles.
org.springframework.web.context.request.RequestContextListener
C'est à peu près tout - nous avons défini ce nouveauRequestContextListener dans nosweb.xml pour pouvoir accéder à la requête depuis lesUserDetailsService.
6. ModifierAuthenticationFailureHandler
Enfin, modifions nosCustomAuthenticationFailureHandler pour personnaliser notre nouveau message d’erreur.
Nous gérons la situation où l'utilisateur est bloqué pendant 24 heures - et nous informons l'utilisateur que son adresse IP est bloquée car il a dépassé le nombre maximal de tentatives d'authentification incorrectes autorisées:
@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. Conclusion
Il est important de comprendre qu’il s’agit d’une bonne première étape dansdealing with brute-force password attempts, mais aussi qu’il y a place à amélioration. Une stratégie de prévention de la force brute de classe production peut impliquer plus que des éléments qu’un bloc IP.
Lesfull implementation de ce didacticiel se trouvent dansthe github project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.