Spring-Security沒法正常捕捉到UsernameNotFoundException異常

前言

在Web應用開發中,安全一直是很是重要的一個方面。在龐大的spring生態圈中,權限校驗框架也是很是完善的。其中,spring security是很是好用的。今天記錄一下在開發中遇到的一個spring-security相關的問題。java

問題描述

使用spring security進行受權登陸的時候,發現登陸接口沒法正常捕捉UsernameNotFoundException異常,捕捉到的一直是BadCredentialsException異常。咱們的預期是:web

  • UsernameNotFoundException -> 用戶名錯誤
  • BadCredentialsException -> 密碼錯誤

貼幾個比較重要的代碼:redis

1. 登陸業務邏輯

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public JwtAuthenticationResponse login(String username, String password) {
		//構造spring security須要的UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
		//調用authenticationManager.authenticate(upToken)方法驗證
		//該方法將會執行UserDetailsService的loadUserByUsername驗證用戶名
		//以及PasswordEncoder的matches方法驗證密碼
        val authenticate = authenticationManager.authenticate(upToken);
        JwtUser userDetails = (JwtUser) authenticate.getPrincipal();
        val token = jwtTokenUtil.generateToken(userDetails);
        return new JwtAuthenticationResponse(token, userDetails.getId(), userDetails.getUsername());
    }
}

2. spring security 的UserDetailsService 實現類

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        AbstractUser abstractUser = userRepository.findByUsername(username);
		//若是經過用戶名找不到用戶,則拋出UsernameNotFoundException異常
        if (abstractUser == null) {
            throw new UsernameNotFoundException(String.format("No abstractUser found with username '%s'.", username));
        } else {
            return JwtUserFactory.create(abstractUser);
        }
    }
}

3. 登陸接口

try {
    final JwtAuthenticationResponse jsonResponse = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());
    //存入redis
    redisService.setToken(jsonResponse.getToken());
    return ok(jsonResponse);
} catch (BadCredentialsException e) {
	//捕捉到BadCredentialsException,密碼不正確
    return forbidden(LOGIN_PASSWORD_ERROR, request);
} catch (UsernameNotFoundException e) {
	//捕捉到UsernameNotFoundException,用戶名不正確
    return forbidden(LOGIN_USERNAME_ERROR, request);
}

在上述代碼中,若是用戶名錯誤,應該執行

catch (UsernameNotFoundException e) {
    return forbidden(LOGIN_USERNAME_ERROR, request);
}

若是密碼錯誤,應該執行

catch (BadCredentialsException e) {
    return forbidden(LOGIN_PASSWORD_ERROR, request);
}

實際上,無論是拋出什麼錯,最後抓到的都是BadCredentialsExceptionspring

問題定位

debug大法

斷點

跟蹤

通過步進法跟蹤代碼,發現問題所在,位於數據庫

AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)

結論

  1. loadUserByUsername方法確實拋出了UsernameNotFoundException
  2. 走到AbstractUserDetailsAuthenticationProvider的authenticate方法的時候,若是hideUserNotFoundExceptions = true,直接就覆蓋了UsernameNotFoundException異常並拋出BadCredentialsException異常,這也就解釋了,爲何老是捕捉到BadCredentialsException異常

問題解決

既然已經找到了是由於hideUserNotFoundExceptions = true致使的問題,那把hideUserNotFoundExceptions = false不就完事了嗎?json

方案1

參考stackoverflow大神回答安全

修改WebSecurityConfig配置,添加AuthenticationProvider Bean

@Bean
public AuthenticationProvider daoAuthenticationProvider() {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(userDetailsService);
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
    return daoAuthenticationProvider;
}

配置AuthenticationProvider Bean

@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
            .authenticationProvider(daoAuthenticationProvider());
}

方案2

因爲之前項目中也是同樣的技術棧,並且代碼也差很少,登陸這段邏輯能夠說是徹底相同,不過以前就一直都沒有這個問題。反覆查看以後發現,在login的代碼有些不一樣框架

ide

val authenticate = authenticationManager.authenticate(upToken);

前面還有一個ui

//執行UserDetailsService的loadUserByUsername驗證用戶名
userDetailsService.loadUserByUsername(authenticationRequest.getUsername());

該方法會直接拋出UsernameNotFoundException,而不走spring security的AbstractUserDetailsAuthenticationProvider,也就不存在被轉換爲BadCredentialsException了。

可是這個方案有個缺點,

若是驗證用戶名經過之後,再次調用

val authenticate = authenticationManager.authenticate(upToken);

還會再執行一遍

userDetailsService.loadUserByUsername(authenticationRequest.getUsername());

該操做是冗餘的,產生了沒必要要的數據庫查詢工做。

推薦使用方案1

相關文章
相關標籤/搜索