Предотвращение попыток аутентификации методом грубой силы с помощью Spring Security

Предотвращение попыток аутентификации методом грубой силы с помощью Spring Security

1. обзор

В этом кратком руководстве мы реализуем базовое решение дляpreventing brute force authentication attempts с помощью Spring Security.

Проще говоря, мы будем вести учет количества неудачных попыток, исходящих с одного IP-адреса. Если этот конкретный IP-адрес превышает установленное количество запросов - он будет заблокирован на 24 часа.

Дальнейшее чтение:

Введение в безопасность методов Spring

Руководство по безопасности на уровне методов с использованием среды безопасности Spring.

Read more

Пользовательский фильтр в цепочке фильтров безопасности Spring

Краткое руководство, показывающее шаги по добавлению пользовательского фильтра в контексте Spring Security.

Read more

Spring Security 5 для реактивных приложений

Быстрый и практичный пример возможностей фреймворка Spring Security 5 для защиты реактивных приложений.

Read more

2. AuthenticationFailureEventListener

Начнем с определенияAuthenticationFailureEventListener - для прослушивания событийAuthenticationFailureBadCredentialsEvent и уведомления нас об ошибке аутентификации:

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

Обратите внимание, как в случае сбоя аутентификации мы сообщаемLoginAttemptService IP-адрес, с которого произошла неудачная попытка.

3. AuthenticationSuccessEventListener

Давайте также определимAuthenticationSuccessEventListener, который прослушивает событияAuthenticationSuccessEvent и уведомляет нас об успешной аутентификации:

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

Обратите внимание, как - аналогично прослушивателю ошибок, мы уведомляемLoginAttemptService IP-адреса, с которого исходил запрос аутентификации.

4. LoginAttemptService

Теперь - давайте обсудим нашу реализациюLoginAttemptService; Проще говоря - мы храним количество ошибочных попыток на IP-адрес в течение 24 часов:

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

Обратите внимание, какan unsuccessful authentication attempt increases the number of attempts for that IP и успешная аутентификация сбрасывают этот счетчик.

С этого момента это просто вопросchecking the counter when we authenticate.

5. UserDetailsService

Теперь давайте добавим дополнительную проверку в нашу собственную реализациюUserDetailsService; когда мы загружаемUserDetails,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);
        }
    }
}

А вот методgetClientIP():

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

Обратите внимание, что у нас есть дополнительная логика дляidentify the original IP address of the Client. В большинстве случаев в этом нет необходимости, но в некоторых сетевых сценариях это необходимо.

В этих редких случаях мы используем заголовокX-Forwarded-For, чтобы перейти к исходному IP-адресу; вот синтаксис этого заголовка:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Также обратите внимание на еще одну супер-интересную возможность Spring -we need the HTTP request, so we’re simply wiring it in.

Вот это круто. Чтобы это работало, нам нужно добавить вweb.xml быстрый слушатель, и это значительно упростит работу.


    
        org.springframework.web.context.request.RequestContextListener
    

Вот и все - мы определили этот новыйRequestContextListener в нашемweb.xml, чтобы иметь доступ к запросу изUserDetailsService.

6. ИзменитьAuthenticationFailureHandler

Наконец, давайте изменим нашCustomAuthenticationFailureHandler, чтобы настроить новое сообщение об ошибке.

Мы работаем с ситуацией, когда пользователь действительно блокируется на 24 часа - и информируем пользователя о том, что его IP-адрес заблокирован, поскольку он превысил максимально допустимое количество неправильных попыток аутентификации:

@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. Заключение

Важно понимать, что это хороший первый шаг вdealing with brute-force password attempts, но также есть возможности для улучшения. Стратегия предотвращения перебора на уровне производства может включать в себя больше, чем элементы, которые блокирует IP.

full implementation этого руководства можно найти вthe github project - это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.

Related