Evitar tentativas de autenticação de força bruta com segurança de primavera

Evitar tentativas de autenticação de força bruta com segurança de primavera

1. Visão geral

Neste tutorial rápido, implementaremos uma solução básica parapreventing brute force authentication attempts usando Spring Security.

Simplificando - manteremos um registro do número de tentativas com falha originadas de um único endereço IP. Se esse IP específico exceder um número definido de solicitações - ele será bloqueado por 24 horas.

Leitura adicional:

Introdução à Spring Method Security

Um guia para segurança em nível de método usando a estrutura Spring Security.

Read more

Um filtro personalizado na cadeia de filtros do Spring Security

Um guia rápido para mostrar as etapas para adicionar filtro personalizado no contexto do Spring Security.

Read more

Spring Security 5 para aplicações reativas

Um exemplo rápido e prático dos recursos da estrutura Spring Security 5 para proteger aplicativos reativos.

Read more

2. AnAuthenticationFailureEventListener

Vamos começar definindo umAuthenticationFailureEventListener - para ouvir eventosAuthenticationFailureBadCredentialsEvent e nos notificar sobre uma falha de autenticação:

@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());
    }
}

Observe como, quando a autenticação falha, informamos oLoginAttemptService do endereço IP de onde se originou a tentativa malsucedida.

3. AnAuthenticationSuccessEventListener

Vamos também definir umAuthenticationSuccessEventListener - que ouve eventosAuthenticationSuccessEvent e nos notifica sobre uma autenticação bem-sucedida:

@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());
    }
}

Observe como - semelhante ao listener de falha, estamos notificandoLoginAttemptService do endereço IP de origem da solicitação de autenticação.

4. OLoginAttemptService

Agora - vamos discutir nossa implementação deLoginAttemptService; em poucas palavras - mantemos o número de tentativas erradas por endereço IP por 24 horas:

@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;
        }
    }
}

Observe comoan unsuccessful authentication attempt increases the number of attempts for that IP, e a autenticação bem-sucedida redefine esse contador.

A partir deste ponto, é simplesmente uma questão dechecking the counter when we authenticate.

5. OUserDetailsService

Agora, vamos adicionar a verificação extra em nossa implementaçãoUserDetailsService personalizada; quando carregamosUserDetails,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);
        }
    }
}

E aqui está o métodogetClientIP():

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

Observe que temos alguma lógica extra paraidentify the original IP address of the Client. Na maioria dos casos, isso não será necessário, mas em alguns cenários de rede será.

Para esses raros cenários, estamos usando o cabeçalhoX-Forwarded-For para chegar ao IP original; aqui está a sintaxe para este cabeçalho:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Observe também outra capacidade superinteressante que o Spring tem -we need the HTTP request, so we’re simply wiring it in.

Agora, isso é legal. Teremos que adicionar um ouvinte rápido em nossoweb.xml para que isso funcione e torna as coisas muito mais fáceis.


    
        org.springframework.web.context.request.RequestContextListener
    

É isso aí - definimos este novoRequestContextListener em nossoweb.xml para poder acessar a solicitação deUserDetailsService.

6. ModificarAuthenticationFailureHandler

Finalmente - vamos modificar nossoCustomAuthenticationFailureHandler para personalizar nossa nova mensagem de erro.

Estamos lidando com a situação em que o usuário realmente é bloqueado por 24 horas - e estamos informando o usuário que seu IP está bloqueado porque ele excedeu o máximo permitido de tentativas de autenticação incorretas:

@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. Conclusão

É importante entender que este é um bom primeiro passo emdealing with brute-force password attempts, mas também que há espaço para melhorias. Uma estratégia de prevenção de força bruta de grau de produção pode envolver mais do que elementos que um IP bloqueia.

Ofull implementation deste tutorial pode ser encontrado emthe github project - este é um projeto baseado em Eclipse, portanto, deve ser fácil de importar e executar como está.