Рубрики
Без рубрики

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

Блокировка пользователей по IP после ряда неудачных попыток аутентификации – простой механизм, реализованный с помощью Spring Security.

Автор оригинала: Eugen Paraschiv.

1. Обзор

В этом кратком руководстве мы реализуем базовое решение для предотвращения попыток аутентификации с использованием грубой силы с помощью Spring Security.

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

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

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

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

Пружинная защита 5 для реактивных приложений

2. Прослушиватель Событий Сбоя Аутентификации

Давайте начнем с определения 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());
    }
}

Обратите внимание, как при сбое аутентификации мы сообщаем Службе Попыток входа в систему 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 ; проще говоря – мы сохраняем количество неправильных попыток на 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;
        }
    }
}

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

С этого момента это просто вопрос проверки счетчика при аутентификации .

5. Служба UserDetailsService

Теперь давайте добавим дополнительную проверку в нашу пользовательскую UserDetailsService реализацию; когда мы загружаем Данные пользователя , нам сначала нужно проверить, заблокирован ли этот IP-адрес :

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

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

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

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Также обратите внимание на еще одну очень интересную возможность, которую имеет Spring- нам нужен HTTP – запрос, поэтому мы просто подключаем его.

Так вот, это круто. Нам придется добавить быстрого слушателя в ваш 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. Заключение

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

полную реализацию этого учебника можно найти в проекте github – это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.