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.
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.
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.
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á.