源码流程
查看源码发现在JdbcUserDetailsService的loadUserByUsername获取到用户以后,自带了一个状态校验的接口
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ...省略
try {
// 在这个方法中调用了loadUserByUsername方法拿到用户信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
// ...
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 发现这里有一个check方法
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// ...
}
else {
throw exception;
}
}
// ...
}
点进去发现可以自定义任何想要的用户状态检查
/**
* Called by classes which make use of a {@link UserDetailsService} to check the status of
* the loaded <tt>UserDetails</tt> object. Typically this will involve examining the
* various flags associated with the account and raising an exception if the information
* cannot be used (for example if the user account is locked or disabled), but a custom
* implementation could perform any checks it wished.
* <p>
* The intention is that this interface should only be used for checks on the persistent
* data associated with the user. It should not involved in making any authentication
* decisions based on a submitted authentication request.
*
* @author Luke Taylor
* @since 2.0
*
* @see org.springframework.security.authentication.AccountStatusUserDetailsChecker
* @see org.springframework.security.authentication.AccountStatusException
*/
public interface UserDetailsChecker {
/**
* Examines the User
* @param toCheck the UserDetails instance whose status should be checked.
*/
void check(UserDetails toCheck);
}
用户校验
因此我们继承UserDetailsChecker类,重写check方法
public class UserDetailsCheck implements UserDetailsChecker{
@Override
public void check(UserDetails toCheck) {
Assert.isTrue(toCheck instanceof JdbcUserDetails, "用户类型错误!");
JdbcUserDetails userDetails = (JdbcUserDetails) toCheck;
String status = userDetails.getStatus();
int errorNum = userDetails.getLoginErrorNum();
Date date = userDetails.getLoginDate();
switch (status) {
case UserStatusConstant.DISABLE_CODE:
int time = isArriveLoginTime(errorNum, date);
if (time > 0) {
throw new LockedException("连续错误次数过多!请"+time+"分钟后重试");
}
break;
case UserStatusConstant.FREEZE_CODE:
throw new LockedException("账户已被冻结,请联系管理员!");
case UserStatusConstant.STOP_CODE:
throw new DisabledException("账户已被停用,请联系管理员!");
default:
}
}
对象注入
最后把UserDetailsChecker注入进来,这里原本UserDetails是在configure(HttpSecurity http) 方法里面注入的,but没有找到能够把UserDetailsChecker也在config里面放进去的方法。最后看了一下源码,自己定义了一个AuthenticationProvider,把UserDetails和UserDetailsChecker都丢进去。
但是又出现了部分异常提示是英文的问题,反正最后研究了很久,看起来是项目启动的时候,MessageSource加载顺序的问题。正常是应该加载new SpringSecurityMessageSource(),但是自定义的情况下会被另一个覆盖掉。
最后研究了很久,加上了doAfterPropertiesSet方法来指定MessageSource才好的。
@Bean
public UserDetailsService jdbcUserDetailsService() {
return new JdbcUserDetailsService();
}
@Bean
public UserDetailsChecker userDetailsChecker() {
return new UserDetailsCheck();
}
@Bean
public AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService, UserDetailsChecker userDetailsCheck, BCryptPasswordEncoder passwordEncoder) {
DaoAuthenticationProvider impl = new DaoAuthenticationProvider(){
@Override
protected void doAfterPropertiesSet() {
this.setMessageSource(new SpringSecurityMessageSource());
}
};
impl.setUserDetailsService(userDetailsService);
impl.setPreAuthenticationChecks(userDetailsCheck);
impl.setPasswordEncoder(passwordEncoder);
return impl;
}
写在最后
时间稍微有点久了,其实中间遇到了很多坑。
最开始状态校验是直接放在loadUserByUsername方法中的,大概是这样:
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private UserDetailsServiceMapper userDetailsServiceMapper;
@Autowired
private PasswordEncoderConfiguration.CustomizePasswordEncoder customizePasswordEncoder;
@Override
public UserDetails loadUserByUsername(String encodeUserName) throws UsernameNotFoundException, DeclareException {
String userName = customizePasswordEncoder.decodeSm4(encodeUserName);
User user = this.userDetailsServiceMapper.selectUserByUserPhone(userName);
if (user == null) {
throw new UsernameNotFoundException(userName);
}
// 这里加入status的校验
check(user.getStatus());
}
return new JdbcUserDetails(user);
}
public void check(String status) {
switch (status) {
case UserStatusConstant.DISABLE_CODE:
int time = isArriveLoginTime(errorNum, date);
if (time > 0) {
throw new LockedException("连续错误次数过多!请"+time+"分钟后重试");
}
break;
case UserStatusConstant.FREEZE_CODE:
throw new LockedException("账户已被冻结,请联系管理员!");
case UserStatusConstant.STOP_CODE:
throw new DisabledException("账户已被停用,请联系管理员!");
default:
}
}
问题是异常一直抛不出去,看源码的时候发现方法形式如下:
authenticate{
try {
retrieveUser{
// 该方法中调用了loadUserByUsername
// 但是这里的异常,只有UsernameNotFoundException可能被抛出,其他都会被归
// 为InternalAuthenticationServiceException抛出去
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
catch (UsernameNotFoundException ex) {
// 外层也只会捕获UsernameNotFoundException异常,其他异常不管直接向上抛
// 最后直接在modelAndView出去了,返回了一个页面出来。而不是返回值。
}
try {
// 这是我们后来写的check方法
check(user);
catch (AuthenticationException exception){
// ...这里的异常才是用户认证的异常处理
// 因此用户状态校验在这里做是没问题的,不能在内部的loadUserByUsername方法中做
}
}
现在项目的修改已经不少了,刚刚本来为了还原当时的错误,直接在loadUserByUsername中抛了一个
LockedException,竟然正常返回了错误信息也不是一整个页面,可能也有其他的影响因素吧~不去找了。
如果大家看到可以测试一下噢~
===================
。。 ◣▲▲◢。。。
。 . ◢████◣。。
。> ██████<。
。 . ◥████◤。。
。 。 ◥██◤。。。
。。。 ◥◤。。。。
。草莓〔〕棒棒糖。
。。。〔〕。。。。
。。。〔〕。。。。
。。。〔〕。。。。
===================