对于登陆功能来讲,为了防止暴力破解密码,通常会对登陆失败次数进行限定,在必定时间窗口超过必定次数,则锁定帐户,来确保系统安全。本文主要讲述一下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