项目中登录的时候使用的是oauth2获取token进行登录,最近要加入账户锁定机制,连续输错几次密码以后锁定一段时间,如果连续错误次数太多则冻结。
梳理了一下功能:
- 用户状态分为四种:正常、暂时封禁、冻结、停用(停用是人员离职一类的,将账号停用)
- 登录时,先获取用户状态,若非正常用户则直接提示对应信息
- 登录失败时,需要能够进入自定义的失败方法,对用户账户失败次数+1,记录本次登录时间,修改用户状态(若需要)
- 登录成功时,将用户状态修改为正常,连续错误次数置0,记录登录时间。
监听登录成功/失败方法
参考链接:spring-security-oauth2 登录或者认证成功后 做一些操作, 比如登录日志
把源码记录一下,主要的认证方法
- 在ResourceOwnerPasswordTokenGranter类的getOAuth2Authentication方法中获取用户名与密码,进入authenticate方法校验
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
// 获取用户名&密码
String username = parameters.get("username");
String password = parameters.get("password");
// 拿完密码就移除
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
// 在这个方法里面进行用户认证,以及成功/失败的事件发布
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
// ...
}
// 省略...
- 接着进入ProviderManager的authenticate方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ...
// 提供了一系列AuthenticationProvider进行验证
for (AuthenticationProvider provider : getProviders()) {
// ....
try {
// 认证
result = provider.authenticate(authentication);
// ...
}
catch (AccountStatusException e) {
// 捕获异常,注意这个方法,里面会发布失败事件
prepareException(e, authentication);
throw e;
}
catch (InternalAuthenticationServiceException e) {
// 捕获异常,注意这个方法,里面会发布失败事件
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
// ...
if (result != null) {
// ...
if (parentResult == null) {
// 发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
throw lastException;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
// 在此处发布认证失败事件
eventPublisher.publishAuthenticationFailure(ex, auth);
}
由于这两个事件的发布,因此可以实现ApplicationListener来获取到登录成功与失败
登录成功:
@Component
public class SuccessEvent implements ApplicationListener<AuthenticationSuccessEvent> {
@Autowired
private UserDetailsServiceMapper userDetailsServiceMapper;
/**
* 获取token成功事件
* @param event
*/
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
if (!event.getSource().getClass().getName().equals("org.springframework.security.authentication.UsernamePasswordAuthenticationToken")) {
return;
}
if (event.getAuthentication().getPrincipal() instanceof JdbcUserDetails) {
// 这个方式稍微有一点麻烦,不止是登陆事件,一些其他的事件,包括自定义的filter也可能进入该方法
// 所以需要这些判断保证只有用户登录成功才进入这里。
JdbcUserDetails userDetails = (JdbcUserDetails) event.getAuthentication().getPrincipal();
User user = userDetailsServiceMapper.selectUserById(Long.valueOf(userDetails.getUserId()));
// 更新用户状态
updateUserAccountById(user);
}
}
}
登录失败与成功类似,可以直接implement失败事件,这里换了种方式,直接监听AuthenticationFailureBadCredentialsEvent 异常。
- 有点神奇,没有仔细看源码,感觉上是filter与oauth是分了两部分的。在自定义filter中抛出该异常并不会被监听到,需要继承BasicErrorController来捕获,这里不写了,只有获取token相关流程的异常会被这个类监听到。
登录失败
@Component
public class failureEvents {
/**
* @param failure
*/
@EventListener
public void onFailure(AuthenticationFailureBadCredentialsEvent failure) {
if (failure == null) {
return;
}
if (failure.getSource() instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) failure.getSource();
LinkedHashMap details = (LinkedHashMap) authenticationToken.getDetails();
// 更新用户状态
updateUserAccount(username);
}
}
}