Spring security failed to properly catch UsernameNotFoundException exception

Keywords: Java Spring Redis Database

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

  1. The loadUserByUsername method does throw a UsernameNotFoundException
  2. 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.

Recommended scheme 1

Posted by V-Man on Mon, 10 Feb 2020 08:07:01 -0800