SpringSecurity根据自定义异常返回登录错误提示信息(账户锁定)
作者:夢里花落知多少
本文介绍了在SpringSecurity中根据自定义异常返回登录错误提示信息的方法,特别是在账户锁定时,通过记录输错次数、重写校验方法并抛出自定义异常,感兴趣的可以了解一下
一、背景
当前场景:用户在登录失败需要根据不同的场景返回不同的提示信息,例如账号不存在或密码输错提示 “用户名或密码错误”,账号禁用是提示 "账户被锁定"等,默认输错5次密码后账户会被锁定。
需求:当最后一次输错密码时需要给用户提示出 “xxx账户将被锁定” 的提示,而不是提示 “用户名或密码错误”
分析:前四次输错提示 “用户名或密码错误”,第5次输错提示 “xxx账户将被锁定”,那么需要在密码校验失败时,获取到这是第几次输错
二、实现方案
2.1 登录失败记录输错次数
@Component public class LoginFailedListener implements ApplicationListener<AbstractAuthenticationFailureEvent> { @Autowired private CustomProperties customProperties ; @Autowired private UserDao userDao; @Override public void onApplicationEvent(AbstractAuthenticationFailureEvent abstractAuthenticationFailureEvent) { String loginName = abstractAuthenticationFailureEvent.getAuthentication() .getName(); if (log.isInfoEnabled()) { log.info("登录失败 loginName=[{}]", loginName); } if (StringUtils.isBlank(loginName)) { return; } //查询登录账号是否存在 UserDo condition = new UserDo(); condition.setLoginName(loginName); List<UserDo> userDos = userDao.selectByRecord(condition); if (CollectionUtils.isEmpty(userDos)) { return; } UserDo userDo = userDos.get(0); UserDo updateDo = new UserDo(); updateDo.setUserId(userDo.getUserId()); Integer loginFailMaxCount = customroperties.getLoginFailMaxCount(); if (loginFailMaxCount <= userDo.getTryTime() + 1) { //更新用户状态为冻结 updateDo.setStatus(EnumUserStatus.FORBIDDEN.getCode()); updateDo.setTryTime(loginFailMaxCount); if (log.isInfoEnabled()) { log.info("登录次数已达最大次数:{} 冻结账户 loginName=[{}]", loginFailMaxCount, loginName); } } else { //更新尝试次数 updateDo.setTryTime(userDo.getTryTime() + 1); if (log.isInfoEnabled()) { log.info("尝试次数+1 loginName=[{}], tryTime=[{}]", loginName, updateDo.getTryTime()); } } updateDo.setUpdateTime(new Date()); userDao.updateBySelective(updateDo); } }
2.2 重写校验方法,满足条件时抛出自定义异常
- 校验抛出的异常一定要是AuthenticationException及其子类
- 因为创建了监听器ApplicationListener的实现,源码中回调是有条件的,所以最后最好是原样抛出 BadCredentialsException ,否则ApplicationListener将不会被触发
@Slf4j public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider { @Autowired private CustomProperties customProperties ; @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { super.additionalAuthenticationChecks(userDetails, authentication); } catch (AuthenticationException e) { if(e instanceof BadCredentialsException && userDetails instanceof UserDto){ UserDto userDto = (UserDto) userDetails; //先到这里,然后去触发LoginFailedListener,达到账户被锁定这里需要+1 //已经被冻结的不处理,只处理正常用户,并且是达到最大失败次数的那一次 if(EnumUserStatus.NORMAL.getCode().equals(userDto.getStatus()) && Objects.equals(userDto.getTryTime() + 1, customProperties .getLoginFailMaxCount())){ if(log.isErrorEnabled()){ log.error("用户:{} 登录失败次数已达最大,账户将被锁定", userDto.getLoginName()); } throw new BadCredentialsException(e.getMessage(), new LoginFailCountOutException("登录失败次数已达最大")); }else { throw e; } }else { throw e; } } } }
public class LoginFailCountOutException extends AuthenticationException { private static final long serialVersionUID = -8546980609242201580L; /** * Constructs an {@code AuthenticationException} with the specified message and no * root cause. * * @param msg the detail message */ public LoginFailCountOutException(String msg) { super(msg); } public LoginFailCountOutException(String msg, Throwable ex) { super(msg, ex); } }
2.3 装配自定义的AuthenticationProvider
@Bean public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, UserAuthoritiesMapper userAuthoritiesMapper) { DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(); // 对默认的UserDetailsService进行覆盖 authenticationProvider.setUserDetailsService(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder); authenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper); return authenticationProvider; }
WebSecurityConfigurerAdapter 的配置类中注入
@Autowired private AuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); }
2.4 AuthenticationFailureHandler 根据异常返回提示
failureHandler((request, response, ex) -> { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); String errorMessage = this.loginFailureErrorMessage(ex); out.write(JSON.toJSONString(Response.<Void>builder().isSuccess(false) .errorMessage(errorMessage) .build())); out.flush(); out.close(); })
private String loginFailureErrorMessage(AuthenticationException ex) { if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) { if(ex.getCause() != null && ex.getCause() instanceof LoginFailCountOutException){ return "登录失败次数已达最大限制, 账户冻结"; } return "用户名或密码错误"; } if (ex instanceof DisabledException) { return "账户被禁用"; } if (ex instanceof LockedException) { return "账户被锁定"; } if (ex instanceof AccountExpiredException) { return "账户已过期"; } if (ex instanceof CredentialsExpiredException) { return "密码已过期"; } log.warn("不明原因登录失败", ex); return "登录失败"; }
到此这篇关于SpringSecurity根据自定义异常返回登录错误提示信息(账户锁定)的文章就介绍到这了,更多相关SpringSecurity自定义异常返回登录错误提示内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!