對於登陸功能來講,爲了防止暴力破解密碼,通常會對登陸失敗次數進行限定,在必定時間窗口超過必定次數,則鎖定帳戶,來確保系統安全。本文主要講述一下spring security的帳戶鎖定。java
spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/core/userdetails/UserDetails.javaredis
/** * 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 <code>true</code> 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 <code>true</code> 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(); }
spring security的UserDetails內置了isAccountNonLocked方法來判斷帳戶是否被鎖定
spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.javaspring
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); } //...... }
AbstractUserDetailsAuthenticationProvider的authenticate裏頭內置了preAuthenticationChecks和postAuthenticationChecks,而preAuthenticationChecks使用的是DefaultPreAuthenticationChecks默認的DaoAuthenticationProvider繼承自AbstractUserDetailsAuthenticationProvider安全
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")); } } }
這裏會對帳戶的isAccountNonLocked進行判斷,若是被鎖定,則在登陸的時候,拋出LockedException
實現大體思路就是基於用戶登陸失敗次數進行時間窗口統計,超過閾值則將用戶的isAccountNonLocked設置爲true,那麼在下次登陸時,則會拋出LockedException。app
這裏基於AuthenticationFailureBadCredentialsEvent事件來實現
時間窗口統計使用ratelimitj-inmemory組件
<dependency> <groupId>es.moki.ratelimitj</groupId> <artifactId>ratelimitj-inmemory</artifactId> <version>0.4.1</version> </dependency>
分佈式場景能夠替換爲基於redis實現
在登陸失敗的時候,spring security會拋出AuthenticationFailureBadCredentialsEvent事件,基於事件監聽機制,能夠實現分佈式
@Component public class LoginFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(LoginFailureListener.class); //錯誤了第四次返回true,而後鎖定帳號,第五次即便密碼正確也會報帳戶鎖定 Set<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); } } }
這裏排除了用戶名錯誤的狀況。而後每失敗一次,就進行時間窗口統計,若是超出閾值,則立馬更新用戶的accountNonLocked屬性。那麼第四次輸錯密碼時,user的accountNonLocked屬性被更新爲false,以後第五次不管密碼對錯,則會拋出LockedException
上面的方案,還須要在時間窗口以後重置這個accountNonLocked屬性,這裏沒有實現。ide
spring security仍是蠻強大的,在AbstractUserDetailsAuthenticationProvider的authenticate裏頭內置了preAuthenticationChecks,幫你創建關於登陸前的各類預校驗。具體的實現就交給應用層。post