美文网首页Spring Security
聊聊spring security的账户锁定

聊聊spring security的账户锁定

作者: go4it | 来源:发表于2017-12-21 09:13 被阅读63次

    对于登录功能来说,为了防止暴力破解密码,一般会对登录失败次数进行限定,在一定时间窗口超过一定次数,则锁定账户,来确保系统安全。本文主要讲述一下spring security的账户锁定。

    UserDetails

    spring-security-core-4.2.3.RELEASE-sources.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 <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方法来判断账户是否被锁定

    AbstractUserDetailsAuthenticationProvider#authenticate

    spring-security-core-4.2.3.RELEASE-sources.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);
        }
        //......
    }
    

    AbstractUserDetailsAuthenticationProvider的authenticate里头内置了preAuthenticationChecks和postAuthenticationChecks,而preAuthenticationChecks使用的是DefaultPreAuthenticationChecks

    默认的DaoAuthenticationProvider继承自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"));
                }
            }
        }
    

    这里会对账户的isAccountNonLocked进行判断,如果被锁定,则在登录的时候,抛出LockedException

    实现账户锁定

    实现大致思路就是基于用户登录失败次数进行时间窗口统计,超过阈值则将用户的isAccountNonLocked设置为true,那么在下次登录时,则会抛出LockedException。

    这里基于AuthenticationFailureBadCredentialsEvent事件来实现
    时间窗口统计使用ratelimitj-inmemory组件

            <dependency>
                <groupId>es.moki.ratelimitj</groupId>
                <artifactId>ratelimitj-inmemory</artifactId>
                <version>0.4.1</version>
            </dependency>
    

    分布式场景可以替换为基于redis实现

    AuthenticationFailureBadCredentialsEvent

    在登录失败的时候,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属性,这里没有实现。

    小结

    spring security还是蛮强大的,在AbstractUserDetailsAuthenticationProvider的authenticate里头内置了preAuthenticationChecks,帮你建立关于登录前的各种预校验。具体的实现就交给应用层。

    相关文章

      网友评论

        本文标题:聊聊spring security的账户锁定

        本文链接:https://www.haomeiwen.com/subject/yyeowxtx.html