May be sometimes you wanna limit your users login attempt for better security, to protect them from buitforce attack, so that their account isn’t compromised. I’m gonna show you how you can limit login attempts and control flooding.
1. Dependency
We’ll use Googles Guava for caching, so please add this dependency in your pom.xml file.
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>
2. Login Attempt Service
To control login attemps determine if it’s blocked we’ll create a service called LoginAttemptService.
@Service public class LoginAttemptService { private final int MAX_ATTEMPT = 10; private LoadingCache<String, Integer> attemptsCache; public LoginAttemptService() { super(); attemptsCache = CacheBuilder.newBuilder(). expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Integer>() { 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; } } }
3. Authentication Listeners
Now we’ll need two listeners AuthenticationSuccessEventListener and AuthenticationFailureListener to listen applications login attempt and know if user login attempt is successful or not.
AuthenticationSuccessEventListener.java
@Component public class AuthenticationSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> { private final LoginAttemptService loginAttemptService; @Autowired public AuthenticationSuccessEventListener(LoginAttemptService loginAttemptService) { this.loginAttemptService = loginAttemptService; } public void onApplicationEvent(AuthenticationSuccessEvent e) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest(); loginAttemptService.loginSucceeded(request.getRemoteAddr()); } }
AuthenticationFailureEventListener.java
@Component public class AuthenticationFailureEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { private final LoginAttemptService loginAttemptService; @Autowired public AuthenticationFailureEventListener(LoginAttemptService loginAttemptService) { this.loginAttemptService = loginAttemptService; } public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest(); loginAttemptService.loginFailed(request.getRemoteAddr()); } }
These events will be fired when user will be trying to log in to the application. But this won’t work yet. You need to let spring know that you’ve created those listeners and use them. So configure beans of those classes in your class annotated with @Configuration annotation.
@Configuration public class AppConfig extends WebMvcConfigurerAdapter{ @Bean public ApplicationListener loginSuccessListener(){ return new AuthenticationSuccessEventListener(); } @Bean public ApplicationListener loginFailureListener(){ return new AuthenticationFailureListener(); } }
4. Block Future Login Attemps in userdetailsservice
Finally we’ll block this user in case of too much login attempts, in this case 10 that was defined on LoginAttemptService.
But since we’ll need to inject/autowire HttpServletRequest we need to configure a bean in AppConfig.java
@Bean public RequestContextListener requestContextListener(){ return new RequestContextListener(); }
Block in UserDetailsService:
CustomUserDetailsService.java
@Service public class CustomUserDetailsService implements UserDetailsService { private final UserService userService; private final LoginAttemptService loginAttemptService; private final HttpServletRequest request; @Autowired public CustomUserDetailsService(UserService userService, LoginAttemptService loginAttemptService, HttpServletRequest request) { this.userService = userService; this.loginAttemptService = loginAttemptService; this.request = request; } @Override public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { String ip = getClientIP(); if (loginAttemptService.isBlocked(ip)) { throw new RuntimeException("blocked"); } User user = this.userService.findOne(id); if (user == null) throw new UsernameNotFoundException("You are not registered yet. Please register first!"); return user; } private String getClientIP() { String xfHeader = request.getHeader("X-Forwarded-For"); if (xfHeader == null){ return request.getRemoteAddr(); } return xfHeader.split(",")[0]; } }
Check it out guys, you’re controlling flood in South East Asia!
Both constructor of theses class
AuthenticationSuccessEventListener
AuthenticationFailureListener
need a parameter… but in AppConfig you pass nothing.
This comment has been removed by the author.
Hi Marc,
Thanks for the comment. Spring automatically injects LoginAttemptService bean here since we've used constructor injection.
So what you'll have to do is, autowire those beans and use them. Here I user new as a demonstration. I apologize for the inconvenience. I'll change it.