Предотвращение попыток аутентификации методом грубой силы с помощью Spring Security
1. обзор
В этом кратком руководстве мы реализуем базовое решение дляpreventing brute force authentication attempts с помощью Spring Security.
Проще говоря, мы будем вести учет количества неудачных попыток, исходящих с одного IP-адреса. Если этот конкретный IP-адрес превышает установленное количество запросов - он будет заблокирован на 24 часа.
Дальнейшее чтение:
Введение в безопасность методов Spring
Руководство по безопасности на уровне методов с использованием среды безопасности Spring.
Пользовательский фильтр в цепочке фильтров безопасности Spring
Краткое руководство, показывающее шаги по добавлению пользовательского фильтра в контексте Spring Security.
Spring Security 5 для реактивных приложений
Быстрый и практичный пример возможностей фреймворка Spring Security 5 для защиты реактивных приложений.
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, поэтому его должно быть легко импортировать и запускать как есть.