前言
在Web應用開發中,安全一直是非常重要的一個方面。在龐大的spring生態圈中,權限校驗框架也是非常完善的。其中,spring security是非常好用的。今天記錄一下在開發中遇到的一個spring-security相關的問題。
問題描述
使用spring security進行授權登錄的時候,發現登錄接口無法正常捕捉UsernameNotFoundException異常,捕捉到的一直是BadCredentialsException異常。我們的預期是:
- UsernameNotFoundException -> 用戶名錯誤
- BadCredentialsException -> 密碼錯誤
貼幾個比較重要的代碼:
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);
}
實際上,不管是拋出什么錯,最后抓到的都是BadCredentialsException
問題定位
debug大法
斷點
跟蹤
經過步進法跟蹤代碼,發現問題所在,位於
AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)
結論
- loadUserByUsername方法確實拋出了UsernameNotFoundException
- 走到AbstractUserDetailsAuthenticationProvider的authenticate方法的時候,如果hideUserNotFoundExceptions = true,直接就覆蓋了UsernameNotFoundException異常並拋出BadCredentialsException異常,這也就解釋了,為什么總是捕捉到BadCredentialsException異常
問題解決
既然已經找到了是因為hideUserNotFoundExceptions = true
導致的問題,那把hideUserNotFoundExceptions = false
不就完事了嗎?
方案1
修改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的代碼有些不同
在
val authenticate = authenticationManager.authenticate(upToken);
前面還有一個
//執行UserDetailsService的loadUserByUsername驗證用戶名
userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
該方法會直接拋出UsernameNotFoundException,而不走spring security的AbstractUserDetailsAuthenticationProvider,也就不存在被轉換為BadCredentialsException了。
但是這個方案有個缺點,
如果驗證用戶名通過以后,再次調用
val authenticate = authenticationManager.authenticate(upToken);
還會再執行一遍
userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
該操作是冗余的,產生了不必要的數據庫查詢工作。