博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
聊聊spring security的账户锁定
阅读量:5883 次
发布时间:2019-06-19

本文共 8640 字,大约阅读时间需要 28 分钟。

  hot3.png

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

UserDetails

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/core/userdetails/UserDetails.java

/** * Provides core user information. * * 

* 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. *

* 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 null. * * @return the authorities, sorted by natural key (never null) */ Collection

getAuthorities(); /** * Returns the password used to authenticate the user. * * @return the password */ String getPassword(); /** * Returns the username used to authenticate the user. Cannot return null * . * * @return the username (never null) */ 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), * false if no longer valid (ie expired) */ boolean isAccountNonExpired(); /** * Indicates whether the user is locked or unlocked. A locked user cannot be * authenticated. * * @return true if the user is not locked, false 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), * false if no longer valid (ie expired) */ boolean isCredentialsNonExpired(); /** * Indicates whether the user is enabled or disabled. A disabled user cannot be * authenticated. * * @return true if the user is enabled, false 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组件

es.moki.ratelimitj
ratelimitj-inmemory
0.4.1

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

AuthenticationFailureBadCredentialsEvent

在登录失败的时候,spring security会抛出AuthenticationFailureBadCredentialsEvent事件,基于事件监听机制,可以实现

@Componentpublic class LoginFailureListener implements ApplicationListener
{ private static final Logger LOGGER = LoggerFactory.getLogger(LoginFailureListener.class); //错误了第四次返回true,然后锁定账号,第五次即使密码正确也会报账户锁定 Set
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,帮你建立关于登录前的各种预校验。具体的实现就交给应用层。

转载于:https://my.oschina.net/go4it/blog/1592930

你可能感兴趣的文章
mybatis数据处理的几种方式
查看>>
作业2
查看>>
raid技术-研究感受
查看>>
远程主机探测技术FAQ集 - 扫描篇
查看>>
C++中调用python函数
查看>>
Nomad添加acl认证
查看>>
“TI门外汉”网路知识笔记一 OSI参考模型
查看>>
你不需要jQuery(五)
查看>>
DatanodeDescriptor说明
查看>>
ServlertContext
查看>>
eclipse编辑器生命周期事件监听
查看>>
Python WOL/WakeOnLan/网络唤醒数据包发送工具
查看>>
sizeof(long)
查看>>
pxe网络启动和GHOST网克
查看>>
2.5-saltstack配置apache
查看>>
django数据库中的时间格式与页面渲染出来的时间格式不一致的处理
查看>>
Python学习笔记
查看>>
java String
查看>>
renhook的使用
查看>>
DOCKER windows 7 详细安装教程
查看>>