Preface
In Web application development, security has always been a very important aspect. In the huge spring ecosystem, the authority verification framework is also very perfect. Among them, spring security is very easy to use. Today, I'd like to record a spring security related problem encountered in development.
Problem description
When using spring security to authorize login, it is found that the login interface cannot normally catch the UsernameNotFoundException exception exception exception, which has been the BadCredentialsException exception. Our expectation is:
- Usernamenotfoundexception - > user name error
- Badcredentialsexception - > password error
Post some important codes:
1. Login business logic
@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) { //UsernamePasswordAuthenticationToken required to construct spring security UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password); //Call the authenticationManager.authenticate(upToken) method to verify //This method will perform the loadUserByUsername authentication of UserDetailsService //And the match method of PasswordEncoder to verify the password val authenticate = authenticationManager.authenticate(upToken); JwtUser userDetails = (JwtUser) authenticate.getPrincipal(); val token = jwtTokenUtil.generateToken(userDetails); return new JwtAuthenticationResponse(token, userDetails.getId(), userDetails.getUsername()); } }
2. Implementation class of UserDetailsService of spring security
@Service public class JwtUserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { AbstractUser abstractUser = userRepository.findByUsername(username); //If the user cannot be found through the user name, a UsernameNotFoundException exception is thrown if (abstractUser == null) { throw new UsernameNotFoundException(String.format("No abstractUser found with username '%s'.", username)); } else { return JwtUserFactory.create(abstractUser); } } }
3. Login interface
try { final JwtAuthenticationResponse jsonResponse = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword()); //Deposit in redis redisService.setToken(jsonResponse.getToken()); return ok(jsonResponse); } catch (BadCredentialsException e) { //Bad credentials exception caught, incorrect password return forbidden(LOGIN_PASSWORD_ERROR, request); } catch (UsernameNotFoundException e) { //Caught UsernameNotFoundException, incorrect user name return forbidden(LOGIN_USERNAME_ERROR, request); }
In the above code, if the user name is wrong, you should execute
catch (UsernameNotFoundException e) { return forbidden(LOGIN_USERNAME_ERROR, request); }
If the password is wrong, you should execute
catch (BadCredentialsException e) { return forbidden(LOGIN_PASSWORD_ERROR, request); }
In fact, no matter what error is thrown, the last catch is BadCredentialsException
Problem location
debug grand law
breakpoint
Track
After step-by-step tracking of the code, the problem is found at
AbstractUserDetailsAuthenticationProvider public Authentication authenticate(Authentication authentication)
conclusion
- The loadUserByUsername method does throw a UsernameNotFoundException
- When going to the authenticate method of AbstractUserDetailsAuthenticationProvider, if hideUserNotFoundExceptions = true, the UsernameNotFoundException exception exception is directly overridden and BadCredentialsException exception is thrown, which explains why BadCredentialsException exception is always caught
Problem solving
Now that the problem caused by hideUserNotFoundExceptions = true has been found, isn't it all over if hideUserNotFoundExceptions = false?
Option 1
Refer to stackoverflow's answer
Modify WebSecurityConfig configuration and add AuthenticationProvider Bean
@Bean public AuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); return daoAuthenticationProvider; }
Configure AuthenticationProvider Bean
@Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .authenticationProvider(daoAuthenticationProvider()); }
Option 2
Since the same technology stack and similar code were used in previous projects, the logics of login can be said to be exactly the same, but there has been no such problem before. After repeatedly checking, it is found that the code of login is somewhat different
stay
val authenticate = authenticationManager.authenticate(upToken);
There's another one in front
//Perform loadUserByUsername authentication for UserDetailsService userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
This method will throw UsernameNotFoundException directly, and without the AbstractUserDetailsAuthenticationProvider of spring security, there will be no conversion to BadCredentialsException.
But there is a drawback,
If the user name passes the verification, call again
val authenticate = authenticationManager.authenticate(upToken);
I'll do it again
userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
This operation is redundant, resulting in unnecessary database query.