Spring Security Resolution (2) - Authentication Process
When learning Spring Cloud, when encountering the related content of authorized service oauth, he always knows half of it, so he decided to study and collate the content, principle and design of Spring Security, Spring Security Oauth2 and other permissions, authentication-related content. This series of articles is written to enhance the impression and understanding in the process of learning. If there is any infringement, please let me know.
Project environment:
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
Configuration Analysis of @EnableGlobalAuthentication
_Remember the reference in the previous article to @Enable Web Security in authorization to the WebSecurity Configuration configuration class and @Enable Global Authentication annotations? At that time, I just explained the next WebSecurity Configuration configuration class, this time it's @EnableGlobalAuthentication configuration.
Look at the @EnableGlobalAuthentication annotation source code and you can see that it refers to the Authentication Configuration configuration class. One of the methods worth our attention is getAuthentication Manager (). (Remember that Authentication Manager (). authenticate () was called to authenticate during the authorization process?) Let's look at the general logic within the source code:
public AuthenticationManager getAuthenticationManager() throws Exception { ...... // 1 Call the authenticationManagerBuilder method to get the authenticationManagerBuilder object for building the authenticationManager object AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder( this.objectPostProcessor, this.applicationContext); ..... // 2. Build method calls, like webSecurity.build() in the authorization process, are built through the performBuild() method in the parent AbstractConfiguredSecurityBuilder.doBuild() method, except that this is no longer through its subclass HttpSecurity.performBuild(), but through Authentication Builder. perfo. RmBuild () authenticationManager = authBuilder.build(); ....... return authenticationManager; }
According to the source code, we can summarize its logic in two parts:
- 1. Get the authenticationManagerBuilder object by calling the authenticationManagerBuilder() method
- 2. Call build() of the authenticationManagerBuilder object to create the authenticationManager object and return
_Let's take a closer look at the process of this build, and we can see that its build call is constructed through performance Build () in the AbstractConfigured SecurityBuilder. doBuild () method, just like the build security FilterChain in the authorization process, but this time it is no longer a call to its subclass HttpSecurity.performBuild(). It's Authentication Manager Builder. performBuild().
Let's look at the internal implementation of the Authentication ManagerBuilder. performBuild () method:
protected ProviderManager performBuild() throws Exception { if (!isConfigured()) { logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null."); return null; } // 1 Creates a ProviderManager object with authenticationProviders parameters ProviderManager providerManager = new ProviderManager(authenticationProviders, parentAuthenticationManager); if (eraseCredentials != null) { providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials); } if (eventPublisher != null) { providerManager.setAuthenticationEventPublisher(eventPublisher); } providerManager = postProcess(providerManager); return providerManager; }
_Here we focus on creating a Provider Manager (Provider Manager is the implementation class of Authentication Manager) object with the parameters of authenticationProviders and returning it.
In retrospect, let's look at the Authentication Manager interface source code:
public interface AuthenticationManager { // Authentication Interface Authentication authenticate(Authentication authentication) throws AuthenticationException; }
As you can see, there is only one authenticate() that we mentioned in the authorization process, whose interface receives an Authentication (we are not unfamiliar with this object, and the UsernamePassword Authrntication Token and others mentioned in the previous authorization process are all subclasses of its implementation) as parameters.
_So far, some key classes or interfaces of authentication have emerged. They are Authentication Manager, Provider Manager, Authentication Provider and Authentication respectively. Next, we will analyze these classes or interfaces.
Authentication Manager
_As we have seen before, it is the entrance to the entire authentication, and its defined authentication interface, authenticate(), receives an Authentication object as a parameter. Authentication Manager only provides an authentication interface method, because in actual use, we not only have the login mode of account password, but also the login mode of SMS authentication code, mailbox login and so on, so it does not authenticate itself, its specific authentication is the Provider Manager subclass, but as we said. There are many authentication methods. If we only rely on Provider Manager itself to implement the authenticate() interface, then we must support so many authentication methods and not write many if judgments. And if we want to support fingerprint login in the future, we have to add an if in this method, which is not conducive to system expansion. Provider Manager itself maintains a List < Authentication Provider > list for storing multiple authentication methods, and then invokes Authentication Provider to truly implement authentication logic through delegation. Authentication is the information that we need to authenticate (not only account information, of course). Authentication returned after authenticate() interface authentication is an object that has been identified and authenticated successfully. Why should we explain the relationship among Authentication Manager, Provider Manager and Authentication Provider? It's easy to confuse them at first. I believe it's easier to understand them after such a description.
III. Authentication
If you haven't seen the source code, you may think that Authentication is a class, but in fact it is an interface. There are no attribute fields in it. It only defines and standardizes the interface methods needed by the authentication object. Let's see what interface methods it defines and what they are. What effect:
public interface Authentication extends Principal, Serializable { // 1. Getting permission information (not only understanding non-role permissions, but also menu permissions, etc.) by default is the implementation class of the GrantedAuthority interface. Collection<? extends GrantedAuthority> getAuthorities(); // 2. Obtain user password information and it will be deleted after authentication is successful. Object getCredentials(); // 3. Mainly store access information such as ip Object getDetails(); // Four key points!! The most important identity information. In most cases, it's the implementation class of the UserDetails interface, such as the User object we configured earlier. Object getPrincipal(); // 5 Certification (Success) boolean isAuthenticated(); // 6 Setting Authentication Identification void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
Since Authentication defines these interface methods, its subclass implementations must be customized according to this standard or specification. Here, we do not list the specific implementations of its subclasses. Interested students can take a look at our most commonly used Username Password Authentication Token implementations (packages). Include its parent class AbstractAuthentication Token)
Provider Manager
It is one of the implementation subclasses of Authentication Manager and one of our most commonly used implementations. As we mentioned earlier, a List < Authentication Provider > object is maintained internally to support and extend various forms of authentication. Let's look at the source code for its implementation of authenticate():
public Authentication authenticate(Authentication authentication) throws AuthenticationException { ...... // 1. Get the inner-maintained List < Authentication Provider > object by getProviders() method and authenticate it by traversing. If the authentication is successful, break it. for (AuthenticationProvider provider : getProviders()) { // 2 As we have seen before, there are many Authentication Provider implementations. Is it not efficient to use the next Authentication Provider after each failure? So here, support () method is used to verify whether the Authentication Provider can be used to verify, instead of directly replacing the next one. if (!provider.supports(toTest)) { continue; } try { // 3 key points, here is to call the authentic 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 The previous authentication was unsuccessful, calling the parent class (strictly speaking, not calling the parent class, but other Authentication Manager implementation classes) authentication method result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // 5 Delete password information after successful authentication to ensure 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; }
_carding the internal implementation logic of the whole method:
- Get the internally maintained List object by getProviders() method and authenticate it by traversal
- Provider. support () method is used to verify whether the current Authentication Provider can be used to verify the validity of the next (in fact, the method is to verify whether the current Authentication object is a subclass of it, such as the support method of DaoAuthentication Provider, which is most commonly used by us). To determine whether the current Authentication is UsernamePassword Authentication Token
- Call its true authentication implementation through provider.authenticate()
- If all previous Authentication Providers fail to authenticate successfully, try calling the parent.authenticate() method: calling the parent class (strictly speaking, not the parent class, but other Authentication Manager implementation classes) authentication method
- Finally, the password information after authentication is deleted through ((Credentials Container) result. erase Credentials () to ensure security.
V. Authentication Provider (Dao Authentication Provider)
As we imagine, Authentication Provider is an interface, which defines an authenticate authentication interface method like Authentication Manager, plus a support () to determine whether the current Authentication can be processed.
public interface AuthenticationProvider { // Defining Authentication Interface Method Authentication authenticate(Authentication authentication) throws AuthenticationException; // Define the interface method for judging whether authentication can be processed boolean supports(Class<?> authentication); }
Here we look at the internal abstractions of the Authentication Provider implementation class DaoAuthentication Provider, which we use most frequently (note that similar to UsernamePasswordAuthentication Filter, the parent class implements the interface, and then the internal processing method calls its subclasses for processing). Implementation of the method:
- Support implementation:
public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); }
_You can see that only judging whether the current authentication is UsernamePassword Authentication Token (or its subclasses)
- Implementation of authrnticate
// 1 Note that the implementation method here is implemented by the parent class AbstractUserDetails Authentication Provider of DaoAuthentication Provider public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 2 Get user name from authentication String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 3 Getting UserDetails Information from the Cache for Successful Authentication Based on username UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // 4. If there is no user information in the cache, the user information needs to be obtained (implemented by DaoAuthentication Provider) user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { ...... } } try { // 5 Pre-check whether the account is locked, expired, frozen (implemented by DefaultPreAuthentication Checks class) preAuthenticationChecks.check(user); // 6. Mainly verify whether the obtained user password is consistent with the incoming user password. additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { // Here, the official discovery that caching may cause some problems and re-authenticate 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 Post-check if user password expires postAuthenticationChecks.check(user); // 8. User information stored in cache after successful validation if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } // 9 Create a UsernamePasswordAuthenticationToken object with authenticated as true (that is, authenticated successfully) and return it return createSuccessAuthentication(principalToReturn, authentication, user); }
_comb the internal implementation logic of authenticate (the implementation of the method here is provided by AbstractUser Details Authentication Provider):
- Get username information from the entry authentication object
- (Cache processing is ignored here) Call the retrieveUser() method (implemented by DaoAuthentication Provider) to retrieve the UserDetails ** object ** from the system (generally from the database) based on username
- PreAuthentication Checks. check () method is used to detect whether the currently acquired UserDetails are expired, frozen, locked (if any condition is true, the corresponding exception will be thrown)
- Determine password consistency by additional Authentication Checks ((implemented by Dao Authentication Provider)
- Detection of UserDetails password expiration by postAuthentication Checks. check ()
- Finally, create a UsernamePassword Authentication Token object with authenticated as true (that is, authenticated successfully) and return it through createSuccess Authentication ().
_Although we know its validation logic, we don't know much about its internal implementation, how to design a new key authentication class, UserDetails, and how to verify its expiration, etc.
User Details Service and User Details
_Continue to take a closer look at the retrieveUser() method. First of all, we notice that its return object is a UserDetails, so let's start with UserDetails.
UserDetails:
Let's first look at the UserDetails source code:
public interface UserDetails extends Serializable { // 1, like Authentication, is to obtain permission information Collection<? extends GrantedAuthority> getAuthorities(); // 2 Get the user's correct password String getPassword(); // 3 Get Account Name String getUsername(); // 4. Does the account expire? boolean isAccountNonExpired(); // 5 Account Locked boolean isAccountNonLocked(); // 6 Password expires boolean isCredentialsNonExpired(); // 7 Account Freezing boolean isEnabled(); }
From the 4, 5, 6, 7 interfaces above, we can know how preAuthentication Checks. check () and postAuthentication Checks. check () are detected. The detection details of these two methods are no longer in-depth study. Interested students can see the source code, as long as we know that detection failure will throw an exception. Now.
_Look, this UserDetails and Authentication are very similar, in fact, there is a real relationship between them. When creating Success Authentication () preaches Authentication objects, its authorities are introduced by UserDetails.
UserDetailsService:
_retrieveUser() method is the only way for the system to obtain the corresponding account information through the incoming account name. Let's look at its internal source code logic:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // Getting user information through 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"); } return loadedUser; } catch (UsernameNotFoundException ex) { ...... } }
_Believe to see here, everything is related, here UserDetails Service. LoadeUserByUsername () is what we implemented ourselves in the last authorization process. The User Details Service source code is no longer posted here.
_There is additional Authentication Checks () password verification is not mentioned, here simply mentioned, its internal is through PasswordEncoder.matches() method for password matching. Note, however, that PasswordEncoder is replaced by Delegating PasswordEncoder by default at the beginning of Security 5. This is also where we discussed earlier that creating User inside the loadUserByUsername method (one of the UserDeatails implementation classes) is a must to use PasswordEncoderFactories. createDelegating Passw. The ordEncoder (). encode () encryption is corresponding.
VII. Personal Summary
Authentication Manager, the top administrator of certification, provides us with the authenticate interface, but we also know that the big boss is not directly involved in the substantive work, so it assigns tasks to its subordinates, that is, the head of our Provider Manager department, and the head of the department. In fact, we also know that department leaders are also directly involved in parameter authentication. They all assign practical tasks to team leaders, namely our Authrntication Provider. Department leaders hold a meeting and gather all team leaders to make them self-sufficient. Judgment (through)
support() Who should complete the tasks handed down by the big boss? When the team leader gets the tasks, he distributes them to all team members. For example, member 1 (User Details Service) only needs to complete retrieveUser() work, then member 2 completes additional Authentication Checks (), and finally the project passes through. Create Success Authentication () reports the results to the team leader, then the team leader reports to the Department leader, who reviews the results, finds that the team leader is not doing well enough, then does some operations (erase Credentials () erase password information), and finally thinks that the results can be reported to the boss. The boss, who doesn't look much at it, gives the results directly to the customers.
According to the usual practice, the flow chart above is as follows:
_This paper introduces the code of authentication process, which can access the security module in the code warehouse. The GitHub address of the project is https://github.com/BUG9/spring-security.
If you are interested in these, you are welcome to support star, follow, collect and forward!