Empêcher les tentatives d’authentification par force brute avec la sécurité Spring

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.

Read more

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.

Read more

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.

Read more

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.