@[TOC] A strange login requirement.

This is a question from my friends in the wechat group, which I find very interesting:

While this is not a typical requirement, getting this out of the way will help you understand Spring Security better.

So, Songo is going to write an article about this topic.

1. Problem recurrence

Maybe some of you don’t understand this question, so LET me explain it a little bit.

When we fail to log in, it may be the wrong username or password, but for security reasons, the server usually does not indicate whether the username is wrong or the password is wrong, but only gives a vague username or password is wrong.

However, for many novice programmers, may not be aware of such “hidden rules”, may give the user a clear hint, a clear hint is wrong username or password.

To avoid this, Spring Security uses encapsulation to hide exceptions where the user name does not exist, resulting in developers getting only a BadCredentialsException at development time, which indicates both that the user name does not exist and that the user password entered incorrectly. Spring Security does this to ensure that our systems are secure enough.

However, for a variety of reasons, sometimes we want to get exceptions where the user does not exist and exceptions where the password is incorrectly entered, which requires some simple customization to Spring Security.

2. Source code analysis

The first thing we need to do is find out why it happened, where it happened.

In Spring Security, there are a number of classes that are responsible for user validation, and I’m not going to list them all here (see Spring Security in Plain English). Here I just say we involve the key class AbstractUserDetailsAuthenticationProvider.

This class is responsible for verifying user names and passwords. Specifically, in the Authenticate method, which is extremely long, I only list the code relevant to this article:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		try {
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (UsernameNotFoundException ex) {
			if (!this.hideUserNotFoundExceptions) {
				throw ex;
			}
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }}}Copy the code

RetrieveUser method is based on user input your user name to look up users, if not found, will be thrown a UsernameNotFoundException, the exception is after the catch, will first determines whether to hide this exception, if you don’t hide, The original exception is thrown intact, or if it needs to be hidden, a new BadCredentialsException is thrown, which is literally an exception for incorrect password entry.

So the core of the problem becomes a hideUserNotFoundExceptions variables.

This is a Boolean type of attribute, the default is true, AbstractUserDetailsAuthenticationProvider also provides for the attribute set method:

public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
	this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
Copy the code

It’s not difficult to look to modify hideUserNotFoundExceptions properties! As long as find AbstractUserDetailsAuthenticationProvider instance, and then call the corresponding set method can be modified.

Now the core of the problem into obtaining AbstractUserDetailsAuthenticationProvider instance from?

See how names, AbstractUserDetailsAuthenticationProvider is an abstract class, so its instance is its subclass instance, subclass is who? , of course, is responsible for user password checking DaoAuthenticationProvider.

Keep that in mind and we’ll use it later.

3. Login process

To understand this, it is also important to understand a general Spring Security authentication process.

First of all, you know that authentication for Spring Security is primarily done by AuthenticationManager, which is an interface whose implementation class is ProviderManager. In short, the specific verification work in Spring Security is the ProviderManager#authenticate method.

However, verification is not done directly by ProviderManager, which manages several AuthenticationProviders. The ProviderManager calls the AuthenticationProvider it manages to complete the verification, as shown below:

ProviderManager, on the other hand, is divided into global and local.

When we log in, the local ProviderManager will verify the user name and password first. If the verification is successful, the user will have logged in successfully. If the verification fails, the parent of the local ProviderManager will be called. In other words, the global ProviderManager completes the verification work. If the verification of the global ProviderManager succeeds, it means that the user has logged in successfully; if the verification of the global ProviderManager fails, it means that the user has logged in failed, as shown in the following figure:

OK, with the above knowledge reserves, we analyze the we want to sell UsernameNotFoundException how to do it.

4. Analysis of ideas

Firstly, our user verification is carried out in the local ProviderManager, which manages several AuthenticationProviders. This several AuthenticationProvider is likely to contain the DaoAuthenticationProvider of what we need. That do we need to call here DaoAuthenticationProvider setHideUserNotFoundExceptions method complete attribute changes?

Songo’s advice is not necessary!

Why is that?

Because when the user logs in, first go to the local ProviderManager to check, if the check is successful of course the best; If validation fails, will not immediately throws an exception, but to the global ProviderManager continue to check, so even if we throw in local ProviderManager UsernameNotFoundException also useless, Because this exception can throw out final call in global ProviderManager (if the global management of ProviderManager DaoAuthenticationProvider didn’t do any special treatment, Then thrown out in local ProviderManager UsernameNotFoundException exception will eventually be hidden).

So, we need to do is to obtain the global ProviderManager, then get to the global ProviderManager managed by DaoAuthenticationProvider, Then call its setHideUserNotFoundExceptions method to modify the corresponding attribute value.

Once you understand the principle, the code is simple.

5. Concrete practice

Global ProviderManager changes in WebSecurityConfigurerAdapter# configure (AuthenticationManagerBuilder) class, This configuration AuthenticationManagerBuilder eventually used to generate the global ProviderManager, so our configuration is as follows:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        auth.authenticationProvider(daoAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .failureHandler((request, response, exception) -> System.out.println(exception)) .permitAll(); }}Copy the code

The code here is simple:

  1. Create a DaoAuthenticationProvider object.
  2. Call object DaoAuthenticationProvider setHideUserNotFoundExceptions method, modified the corresponding attribute values.
  3. Configure the user data source for the DaoAuthenticationProvider.
  4. Set the DaoAuthenticationProvider to auth object, auth will be used to generate global ProviderManager.
  5. In the other configure method, we simply configure the login callback and print an exception if the login fails.

Come on.

Next, launch the project for testing. If you enter an incorrect user name, you can see that the IDEA console prints the following information:

As you can see, UsernameNotFoundException exception has been thrown out.

6. Summary

Well, do it today and friends to share about how to throw in the Spring Security UsernameNotFoundException abnormalities, although this is just a small demand, but can deepen people’s understanding of the Spring Security, Interested partners can carefully consider.

An aside:

Another simple way to implement this requirement is to customize an exception that does not exist for the user. When the user fails to find the exception in the UserDetailsService, the user-defined exception will not be hidden. This is relatively simple, I will not write the code, interested friends can try.