Автор оригинала: 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 LoadingCacheattemptsCache; 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, поэтому его должно быть легко импортировать и запускать как есть.