Through this Spring Security tutorial, I will guide you how to tighten security of a Spring Boot application by implementing limit login attempts function that prevents brute-force passwords guessing which can be exploited by hackers.

You will learn how to implement the limit login attempts function with the following strategy:

Suppose that you’re developing a Java web application based on Spring Boot with authentication already implemented using Spring Security and MySQL database (stores user information). And as common standard, Thymeleaf is used as the template engine, Spring Data JPA and Hibernate in the data access layer, and HTML 5 and Bootstrap for the UI.

 

1. Update Users table and User Entity Class

Suppose that the user information is stored in a table named users. You need to add 3 extra columns in order to implement the limit login attempt function. They are:

The following picture shows structure of the users table with 3 new columns:

table users

Then update the entity class User as follows:

import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	private String email;
	
	private String password;
	
	private String firstName;
	
	private String lastName;
	
	private boolean enabled;
	
	@Column(name = "account_non_locked")
	private boolean accountNonLocked;
	
	@Column(name = "failed_attempt")
	private int failedAttempt;
	
	@Column(name = "lock_time")
	private Date lockTime;

	// constructors...
	
	// getters...
	
	// setters...
}
Here, we add 3 new fields that map to the 3 new columns in the users table accordingly.



 

2. Update UserDetails Class

When implementing authentication using Spring Security, you already created a class of type UserDetails to hold details of an authenticated user. So make sure that the isAccountNonLocked() method is updated as follows:

import org.springframework.security.core.userdetails.UserDetails;


public class ShopmeUserDetails implements UserDetails {
	private User user;
	
	public ShopmeUserDetails(User user) {
		this.user = user;
	}

	@Override
	public boolean isAccountNonLocked() {
		return user.isAccountNonLocked();
	}
	
	// other overridden methods...
}
It’s important because Spring Security will reject authentication if this method returns false.

 

3. Update User Repository and Service Classes

Next, you need to declare a method in the UserRepository class to update the number of failed login attempts for a user based on his email, as follows:

import org.springframework.data.jpa.repository.*;

public interface UserRepository extends JpaRepository<User, Integer> {
		
	@Query("UPDATE User u SET u.failedAttempt = ?1 WHERE u.email = ?2")
	@Modifying
	public void updateFailedAttempts(int failAttempts, String email);
	
}
As you can see, we use a custom query (JPA query).

And update the business class UserServices by implementing 4 new methods and a couple of constants, as follows:

@Service
@Transactional
public class UserServices {

	public static final int MAX_FAILED_ATTEMPTS = 3;
	
	private static final long LOCK_TIME_DURATION = 24 * 60 * 60 * 1000;	// 24 hours
	
	@Autowired
	private UserRepository repo;
	
	public void increaseFailedAttempts(User user) {
		int newFailAttempts = user.getFailedAttempt() + 1;
		repo.updateFailedAttempts(newFailAttempts, user.getEmail());
	}
	
	public void resetFailedAttempts(String email) {
		repo.updateFailedAttempts(0, email);
	}
	
	public void lock(User user) {
		user.setAccountNonLocked(false);
		user.setLockTime(new Date());
		
		repo.save(user);
	}
	
	public boolean unlockWhenTimeExpired(User user) {
		long lockTimeInMillis = user.getLockTime().getTime();
		long currentTimeInMillis = System.currentTimeMillis();
		
		if (lockTimeInMillis + LOCK_TIME_DURATION < currentTimeInMillis) {
			user.setAccountNonLocked(true);
			user.setLockTime(null);
			user.setFailedAttempt(0);
			
			repo.save(user);
			
			return true;
		}
		
		return false;
	}
}
Let me explain this new code. First, we declare the maximum number of failed login attempts allowed:

public static final int MAX_FAILED_ATTEMPTS = 3;
And duration of the lock time in milliseconds:

private static final long LOCK_TIME_DURATION = 24 * 60 * 60 * 1000;	// 24 hours
So it would be easy to configure/change the maximum allowed failed logins and lock duration. And let’s come to the new methods:

 

4. Update Login Page

In your custom login page, ensure that it contains the following code snippet to display the exception message upon failed login.

<div th:if="${param.error}">
	<p class="text-danger">[[${session.SPRING_SECURITY_LAST_EXCEPTION.message}]]</p>
</div>
It’s important to have this code in the login page, so it will show the original error message generated by Spring Security.

To learn more about coding a custom login page with Spring Security, refer to this article.

 

5. Code Authentication Failure Handler

Next, we need to code a custom authentication failure handler class to intervene the authentication process of Spring Security in order to update the number of failed login attempts, lock and unlock the user’s account. So create a new class CustomLoginFailureHandler with the following code:

package com.shopme.admin.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	
	@Autowired
	private UserServices userService; 
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		String email = request.getParameter("email");
		User user = userService.getByEmail(email);
		
		if (user != null) {
			if (user.isEnabled() && user.isAccountNonLocked()) {
				if (user.getFailedAttempt() < UserServices.MAX_FAILED_ATTEMPTS - 1) {
					userService.increaseFailedAttempts(user);
				} else {
					userService.lock(user);
					exception = new LockedException("Your account has been locked due to 3 failed attempts."
							+ " It will be unlocked after 24 hours.");
				}
			} else if (!user.isAccountNonLocked()) {
				if (userService.unlockWhenTimeExpired(user)) {
					exception = new LockedException("Your account has been unlocked. Please try to login again.");
				}
			}
			
		}
		
		super.setDefaultFailureUrl("/login?error");
		super.onAuthenticationFailure(request, response, exception);
	}

}
Let me explain this code. First, it gets a User object based on the email which was entered in the login page. If the user is found in the database and the user is enabled and non-locked:

And in case the user’s account is locked, it will try to unlock the account if lock duration expires.

Note that we throw LockedException (defined by Spring Security) with custom error message that will be displayed in the login page.

Read this article to learn more about authentication failure handler in Spring Security.

 

6. Code Authentication Success Handler

There can be a case in which the user fails to login the first time (or second time) but successful on the next time. So the application should clear the number of failed login attempts immediately after the user has logged in successfully.

To do so, you need to create a custom authentication handler class with the following code:

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

	@Autowired
	private UserServices userService;
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		ShopmeUserDetails userDetails =  (ShopmeUserDetails) authentication.getPrincipal();
		User user = userDetails.getUser();
		if (user.getFailedAttempt() > 0) {
			userService.resetFailedAttempts(user.getEmail());
		}
		
		super.onAuthenticationSuccess(request, response, authentication);
	}
	
}
As you can see, upon the user’s successful login, the application resets the number of failed login attempts to zero.

 

7. Update Spring Security Configuration Class

And to enable the custom authentication failure and success handlers above, you need to update the Spring Security configuration class as follows:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
			.antMatchers("/login").permitAll()
			...
			.anyRequest().authenticated()
			...
			.formLogin()
				.loginPage("/login")
				.usernameParameter("email")
				.failureHandler(loginFailureHandler)
				.successHandler(loginSuccessHandler)				
				.permitAll()
			...
	}
	
	@Autowired
	private CustomLoginFailureHandler loginFailureHandler;
	
	@Autowired
	private CustomLoginSuccessHandler loginSuccessHandler;
}
That’s done for the coding. Next, we’re ready to test the limit login attempt function.

 

8. Test Limit Login Attempts Function

Before testing, ensure that the new columns have default values: 0 for failed_attempt, true for account_non_locked and null for lock_time.

Start your Spring Boot application and go to the login page. Enter a correct username but wrong password, you would see the following error at the first failed login attempt:

first failed login attempt

Check the database and you should see failed_attempt = 1. Now try to login with a wrong password again. You would see the same error, but failed_attempt = 2.

Next, try to make the third failed login, it says the user’s account has been locked:

acount is locked

Check the database, and you should see failed_attempt = 2, account_non_locked = 0 (false) and lock_time is set to a specific time.

Then the user won’t be able to login during 24 hours since his account is locked.

Wait for the lock time expires (for quick testing, you can change the lock time duration to 5 minutes), and login again with correct credentials. The user will see this screen:

account is unlocked

And the user must login again (with correct credentials). Check the database and you should see values of the new 3 columns have been set to default values.

That’s some code examples and references for you to implement the limit login attempt function in a Spring Boot application. To see the coding in action, I recommend you to watch the following video:

 

Related Spring Security Tutorials:

 

Other Spring Boot Tutorials:


About the Author:

is certified Java programmer (SCJP and SCWCD). He started programming with Java in the time of Java 1.4 and has been falling in love with Java since then. Make friend with him on Facebook and watch his Java videos you YouTube.