Spring Security Parsing (2) — Authentication process

In learning Spring Cloud, encountered authorization service oAUth related content, always half-understanding, so I decided to first Spring Security, Spring Security Oauth2 and other permissions, authentication related content, principle and design study and sort out. This series of articles is written in the process of learning to strengthen the impression and understanding, if there is infringement, please inform.

Project Environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

A, @ EnableGlobalAuthentication configuration parsing

Remember the previous interpretation of the authorization process mentioned @ EnableWebSecurity cited WebSecurityConfiguration configuration classes and @ EnableGlobalAuthentication comments? Was explained under WebSecurityConfiguration configuration class, this time it’s @ EnableGlobalAuthentication configuration turn.

See @ EnableGlobalAuthentication annotations source, we can see the quotes AuthenticationConfiguration configuration class. One method worth noting is getAuthenticationManager() (remember that AuthenticationManager().authenticate() was called during authorization?). , let’s take a look at the internal logic of its source code:

public AuthenticationManager getAuthenticationManager() throws Exception { ...... / / 1 call authenticationManagerBuilder method to obtain authenticationManagerBuilder object, Object is used to build the authenticationManager AuthenticationManagerBuilder authBuilder = AuthenticationManagerBuilder ( this.objectPostProcessor, this.applicationContext); . // 2 Build method call is the same as websecurity.build () in the authorization procedure, By the parent class AbstractConfiguredSecurityBuilder. DoBuild () method of performBuild build () method, Is no longer here is through its subclasses HttpSecurity performBuild (), But by AuthenticationManagerBuilder. PerformBuild () the authenticationManager = authBuilder. The build (); .return authenticationManager;
	}
Copy the code

According to the source code, we can summarize its logic into two parts:

  • 1, by calling the authenticationManagerBuilder () method to obtain authenticationManagerBuilder object
  • 2, call authenticationManagerBuilder object the build () create the authenticationManager object and return

Let’s take a closer look at the build process. Can find its build calls to build in the process of authorized securityFilterChain By AbstractConfiguredSecurityBuilder. DoBuild () method of performBuild () To build, but this is no longer to call its subclasses HttpSecurity. PerformBuild (). But AuthenticationManagerBuilder performBuild (). We’ll look at AuthenticationManagerBuilder. PerformBuild () method of the internal implementation:

protected ProviderManager performBuild() throws Exception {
		if(! isConfigured()) { logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
			returnnull; } // 1 creates a ProviderManager object with the authenticationProviders parameter ProviderManager ProviderManager = new ProviderManager(authenticationProviders, parentAuthenticationManager);if(eraseCredentials ! = null) { providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials); }if(eventPublisher ! = null) { providerManager.setAuthenticationEventPublisher(eventPublisher); } providerManager = postProcess(providerManager);return providerManager;
	}
Copy the code

Here we focus on creating an internal ProviderManager object containing the authenticationProviders parameter (The ProviderManager is the implementation class of AuthenticationManager) and returning it.

Back to the source code of the AuthenticationManager interface:

Public interface AuthenticationManager {// Authentication interface Authenticate (Authentication Authentication) throws AuthenticationException; }Copy the code

It can be seen that there is only one authenticate(), which we mentioned in the authorization process, and its interface receives Authentication (we are familiar with this object, Authorization before mentioned UsernamePasswordAuthrnticationToken etc are in the process of its implementation subclass) object as a parameter.

So far, some of the key classes or interfaces for Authentication have surfaced. They are AuthenticationManager, ProviderManager, AuthenticationProvider, Authentication, Let’s take a look around these classes or interfaces.

Second, the AuthenticationManager

As we saw earlier, it is the entry to the entire Authentication, and its defined Authentication interface Authenticate () receives an Authentication object as a parameter. AuthenticationManager only provides an authentication interface method, because in actual use, we not only have the login method of account password, but also SMS verification code login, email login and so on, so it does not do any authentication itself, its specific authentication is ProviderManager subclass, However, as we said, there are many authentication methods. If we only rely on ProviderManager itself to implement authenticate() interface, we need to support so many authentication methods without writing many IF judgments. Moreover, if we want to support fingerprint login in the future, So ProviderManager itself maintains a List

for various authentication methods, and then, through the delegate, Call the AuthenticationProvider to actually implement the authentication logic. Authentication is the information we need to authenticate(of course, not only account information). Authentication returned by authenticate() interface is an object that is identified as the Authentication is successful. AuthenticationManager, ProviderManager, AuthenticationProvider, ProviderManager, providerProvider

Third, the Authentication

If you have not read the source code may think Authentication is a class, but in fact it is an interface, its internal does not exist any attribute fields, it only defines and standardize the Authentication object required interface methods, let’s look at its definition of interface methods, respectively and what effect:

Public interface Authentication extends Principal, Serializable {// public interface Authentication extends Principal, Serializable { The default is the GrantedAuthority interface implementation class Collection<? extends GrantedAuthority> getAuthorities(); // 2 Obtain the user password information, which is deleted after the authentication succeeds. Object getCredentials() // 3 Object getDetails(); // 4 key!! The most important identity information. In most cases it is the implementation class of the UserDetails interface, such as the User Object Object getPrincipal() we configured earlier; // 5 Whether authenticated (successful) Boolean isAuthenticated(); // 6 Set the authentication id voidsetAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Copy the code

Since Authentication defines these interface methods, its subclass implementations must be customized according to this standard or specification. The subclass implementations will not be listed here. Interested students can go to the next We are the most commonly used UsernamePasswordAuthenticationToken (including its parent class AbstractAuthenticationToken)

Four, ProviderManager

It is one of the implementation subclasses of AuthenticationManager and one of the most commonly used implementations. As we mentioned earlier, a List

object is maintained internally to support and extend multiple forms of authentication. Authenticate () ¶

public Authentication authenticate(Authentication authentication) throws AuthenticationException { ...... // 1 Use getProviders() to obtain the internally maintained List<AuthenticationProvider> object and iterate to authenticate it, as long as the authentication succeedsbreak 
		for(AuthenticationProvider provider : GetProviders ()) {// 2 As we saw earlier, there are many AuthenticationProvider implementations out there. Isn't it inefficient to use the next AuthenticationProvider after each AuthenticationProvider failure? So the supports() method is used to verify that this AuthenticationProvider can be used for authentication, not just the next oneif(! provider.supports(toTest)) {continue; } try {// 3 Key: call the real authentication method result = provider.authenticate(authentication);if(result ! = null) { copyDetails(authentication, result);break; } } catch (AccountStatusException e) { prepareException(e, authentication); throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; }}if(result == null && parent ! = null) {try {// 4 But other AuthenticationManager implementation classes) result = parentResult = paren.authenticate (authentication); } catch (ProviderNotFoundException e) { } catch (AuthenticationException e) { lastException = parentException = e; }}if(result ! = null) {if(eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {/ / 5 remove certification after a successful password information, Guaranteed Security ((CredentialsContainer) result).Erasecredentials (); }if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}
        
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}
Copy the code

Sort out the internal implementation logic of the whole method:

  • Get the internally maintained List object using getProviders() and iterate over it to authenticate it
  • The provider. Supports () method is used to verify that the current AuthenticationProvider is valid. It does not replace the current AuthenticationProvider directly. Such as our most commonly used to DaoAuthenticationProvider supports method is to determine whether the current Authentication UsernamePasswordAuthenticationToken)
  • Its real authentication implementation is invoked via provider.authenticate()
  • If none of the previous AuthenticationProviders can successfully authenticate, try calling the parent.authenticate() method: Call the parent (strictly not the parent, but the other AuthenticationManager implementation class) authentication method
  • Finally, delete entials password by ((CredentialsContainer) result).erasecredentials () for security

Five, the AuthenticationProvider (DaoAuthenticationProvider)

As we can imagine, AuthenticationProvider is an interface that defines a Authenticate interface method like AuthenticationManager, A supports() is added to determine whether the current Authentication can be processed.

Public interface AuthenticationProvider {// Defines the Authentication interface method Authentication Authenticate (Authentication Authentication) throws AuthenticationException; Boolean supports(Class<? > authentication); }Copy the code

Here we take, we use the most a AuthenticationProvider implementation class DaoAuthenticationProvider (note that here and UsernamePasswordAuthenticationFilter similar, The interface is implemented through the parent class, and then the inner method calls its subclass.)

  • Supports implementation:
public boolean supports(Class<? > authentication) {return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}
Copy the code

You can see only decide whether the current authentication is a UsernamePasswordAuthenticationToken (or its subclasses)

  • Authrnticate implementation
/ / 1 note here the implementation of the method is the parent class DaoAuthenticationProvider AbstractUserDetailsAuthenticationProvider implementation of public Authentication Authenticate (Authentication Authentication) throws AuthenticationException {// 2 Obtains the user name String from Authentication username = (authentication.getPrincipal() == null) ?"NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true; / / 3 according to the username from the cache access authentication information populated UserDetails successfully populated UserDetails user = this. UserCache. GetUserFromCache (username);if (user == null) {
			cacheWasUsed = false; Try {/ / 4 if the cache no user information needs to get the user information (implemented by DaoAuthenticationProvider) user = retrieveUser (username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { ...... Try {}} / / 5 front check whether the account is locked, expired, freeze (implementation) from class DefaultPreAuthenticationChecks preAuthenticationChecks. Check (user); / / 6 main access to the user's password and verification of the incoming user passwords are consistent additionalAuthenticationChecks (user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception)if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; }} / / 7 rear check whether the user password expired postAuthenticationChecks. Check (user); // 8 The user information is cached after the authentication is successful. cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } / / 9 to create an authenticated to true (that is, certification success) UsernamePasswordAuthenticationToken object and returns the return createSuccessAuthentication(principalToReturn, authentication, user); }Copy the code

Combing the authenticate (where the method implementation is provided by AbstractUserDetailsAuthenticationProvider) within a method implementation logic:

  • The username information is obtained from the input authentication object
  • Ignore the cache (here) call retrieveUser () method (implemented by DaoAuthenticationProvider) according to the username access to the system (in general) from the database to get toUserDetailsobject
  • Through preAuthenticationChecks () method to detect the current access to whether the populated UserDetails expired, freezing and locking (if any conditions to true will throw corresponding exception)
  • Through additionalAuthenticationChecks () () by a DaoAuthenticationProvider implementation to determine whether a password
  • Through postAuthenticationChecks. Check () check whether the password of the populated UserDetails expired
  • Finally, createSuccessAuthentication () to create an authenticated to true (that is, certification success) UsernamePasswordAuthenticationToken object and return

While we know the authentication logic, we don’t know much about the internal implementation, how a key new authentication class, UserDetails, is designed, and how to verify its expiration, etc.

6. UserDetailsService and UserDetails

Taking a closer look at the retrieveUser() method, first we notice that its return object is a UserDetails, so let’s start with the UserDetails.

Populated UserDetails:

Let’s take a look at the UserDetails source code:

Public interface UserDetails extends Serializable {// 1 Extends Serializable {// 1 extends GrantedAuthority> getAuthorities(); // 2 Get the correct password. String getPassword(); String getUsername(); // 4 Account expired Boolean isAccountNonExpired(); // 5 Whether the account is locked Boolean isAccountNonLocked(); // 6 Password expired Boolean isCredentialsNonExpired(); // 7 Whether the account is frozen Boolean isEnabled(); }Copy the code

We can know from the above 4, 7 interface preAuthenticationChecks. Check () and postAuthenticationChecks. Check () is how to detect, two methods of detecting the details here will not get, Interested students can look at the source code, we just know that detection failure will throw an exception on the line.

Looked at bluffing, the populated UserDetails and Authentication are very similar, actually really have relationship between them, in createSuccessAuthentication missionary Authentication object (), Its AUTHORITIES were passed in by UserDetails.

UserDetailsService:

The retrieveUser() method is the only way for the system to retrieve the corresponding account information from the incoming account name. Let’s look at its internal source logic:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); Try {// Obtain user information by using the loadUserByUsername method of UserDetailsService UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			returnloadedUser; } catch (UsernameNotFoundException ex) { ...... }}Copy the code

Believe see here and everything is associated, UserDetailsService. Here loadUserByUsername () is what we in the previous authorization process Our own implementation. I won’t post the UserDetailsService source code here.

And additionalAuthenticationChecks password authentication () no, here under simple, its internal is through PasswordEncoder. Matches () method does the passwords match. But be careful here, Here PasswordEncoder 5 began to default on the Security replaced DelegatingPasswordEncoder here too and we create User before discussing internal loadUserByUsername method (UserDeatails implementation class) is must use PasswordEncoderFactories createDelegatingPasswordEncoder () encode () encryption is appropriate.

Vii. Personal summary

AuthenticationManager, the top administrator for authentication, provides us with the authenticate() interface, but we also know that big boss usually doesn’t get directly involved in the real work, so it assigns tasks to its subordinates, That is, the head of our ProviderManager department, who undertakes the authentication work (the realization of authentication). In fact, we also know that the head of the department is also directly responsible for the authentication work by assigning actual tasks to the head of the group. In other words, our AuthrnticationProvider, the department leaders hold a meeting, gather all the team leaders, and let them judge by themselves (through support()) who should complete the tasks handed by the big boss. After the team leaders get the tasks, they distribute them to all the team members. Such as member 1 (UserDetailsService) only need to complete the retrieveUser () work, then member 2 complete additionalAuthenticationChecks () work, Finally by the project manager (createSuccessAuthentication ()) will report the result to the leader, then the heads of reporting to the department heads, department heads to review the results, think leader is doing is not good enough, Then I did a few more actions (eraseCredentials() to erase the password message) and, deciding the result was good enough, reported it to my boss, who, without looking too far, sent it directly to the customer (Filter).

According to the convention, the above flow chart:

The code that introduces the authentication process can access the security module in the code repository at the github address of the project: github.com/BUG9/spring…

If you are interested in these, welcome to star, follow, favorites, forward to give support!