sequence

To prevent forcible password cracking, the number of failed login attempts is limited. If the number of failed login attempts exceeds a certain number in a certain period of time, the account will be locked to ensure system security. This article focuses on Account locking for Spring Security.

UserDetails

Spring ws-security – core – RELEASE 4.2.3 – sources. The jar! /org/springframework/security/core/userdetails/UserDetails.java

/**
 * Provides core user information.
 *
 * <p>
 * Implementations are not used directly by Spring Security for security purposes. They
 * simply store user information which is later encapsulated into {@link Authentication}
 * objects. This allows non-security related user information (such as email addresses,
 * telephone numbers etc) to be stored in a convenient location.
 * <p>
 * Concrete implementations must take particular care to ensure the non-null contract
 * detailed for each method is enforced. See
 * {@link org.springframework.security.core.userdetails.User} for a reference
 * implementation (which you might like to extend or use in your code).
 *
 * @see UserDetailsService
 * @see UserCache
 *
 * @author Ben Alex
 */
public interface UserDetails extends Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
	 *
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Returns the password used to authenticate the user.
	 *
	 * @return the password
	 */
	String getPassword();

	/**
	 * Returns the username used to authenticate the user. Cannot return <code>null</code>
	 * .
	 *
	 * @return the username (never <code>null</code>)
	 */
	String getUsername();

	/**
	 * Indicates whether the user's account has expired. An expired account cannot be * authenticated. * * @return true if the user's account is valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isAccountNonExpired();

	/**
	 * Indicates whether the user is locked or unlocked. A locked user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 */
	boolean isAccountNonLocked();

	/**
	 * Indicates whether the user's credentials (password) has expired. Expired * credentials prevent authentication. * * @return true if the user's credentials are valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 */
	boolean isEnabled();
}
Copy the code

Spring Security’s UserDetails has the isAccountNonLocked method built in to determine if an account is locked

AbstractUserDetailsAuthenticationProvider#authenticate

Spring ws-security – core – RELEASE 4.2.3 – sources. The jar! /org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private UserCache userCache = new NullUserCache();
	private boolean forcePrincipalAsString = false;
	protected boolean hideUserNotFoundExceptions = true;
	private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
	private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports"."Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (! cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } / /... }Copy the code

The authenticate the AbstractUserDetailsAuthenticationProvider built-in preAuthenticationChecks and postAuthenticationChecks, While preAuthenticationChecks use DefaultPreAuthenticationChecks

The default DaoAuthenticationProvider inherited from AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider#DefaultPreAuthenticationChecks

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
		public void check(UserDetails user) {
			if(! user.isAccountNonLocked()) { logger.debug("User account is locked");

				throw new LockedException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.locked"."User account is locked"));
			}

			if(! user.isEnabled()) { logger.debug("User account is disabled");

				throw new DisabledException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.disabled"."User is disabled"));
			}

			if(! user.isAccountNonExpired()) { logger.debug("User account is expired");

				throw new AccountExpiredException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.expired"."User account has expired")); }}}Copy the code

IsAccountNonLocked is determined, and if locked, a LockedException is thrown at login

Locking accounts

If the number of login failures exceeds the threshold, isAccountNonLocked is set to True. The next login will throw a LockedException.

Here based on AuthenticationFailureBadCredentialsEvent events to implement the time window statistical use ratelimitj – inmemory components

< the dependency > < groupId > es. Moki. Ratelimitj < / groupId > < artifactId > ratelimitj - inmemory < / artifactId > < version > 0.4.1 < / version >  </dependency>Copy the code

Distributed scenarios can be replaced with redis based implementations

AuthenticationFailureBadCredentialsEvent

When login failed, spring security will be thrown AuthenticationFailureBadCredentialsEvent events, mechanism, based on the event listeners can be achieved

@Component public class LoginFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(LoginFailureListener.class); // Error returned for the fourth timetrueSet<RequestLimitRule> rules = collections.singleton (RequestLimitRule. Of (10, TimeUnit.MINUTES,3)); // 3 request per 10 minute, per key RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules); @Autowired UserDetailsManager userDetailsManager; @Override public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {if (event.getException().getClass().equals(UsernameNotFoundException.class)) {
            return;
        }

        String userId = event.getAuthentication().getName();

        boolean reachLimit = limiter.overLimitWhenIncremented(userId);

        if(reachLimit){
            User user = (User) userDetailsManager.loadUserByUsername(userId);

            LOGGER.info("user:{} is locked",user);

            User updated = new User(user.getUsername(),user.getPassword(),user.isEnabled(),user.isAccountNonExpired(),user.isAccountNonExpired(),false,user.getAuthorities()); userDetailsManager.updateUser(updated); }}}Copy the code

The user name error is excluded here. Each time a failure occurs, a time window is counted, and if the threshold is exceeded, the user’s accountNonLocked attribute is updated immediately. The user’s accountNonLocked property is updated to false on the fourth incorrect password, and a LockedException is thrown on the fifth incorrect password

In the scenario above, you also need to reset the accountNonLocked property after the time window, which is not implemented here.

summary

Spring security is pretty strong, at AbstractUserDetailsAuthenticationProvider authenticate with built-in preAuthenticationChecks, Help you set up a variety of pre-verification before login. The implementation is left to the application layer.