Security login authentication process analysis

Posted by James138 on Sat, 25 Sep 2021 12:30:40 +0200

Recently, when I was writing my graduation project, I used this framework. My partner gave me a demand for multiple login methods. He said that only account and password login were not very good, and asked me to add several methods, such as SMS authentication login, email authentication login, third-party login, etc. (the first two have been implemented, and the third-party login has not been completed). It was very confusing at the beginning, There's no way to start.

After reading several blogs, they are incomplete or too advanced. I can't do it. Then I read the blog and said that after understanding the principle and process, it is actually quite simple to write a variety of ways. Then I went to Debug honestly.

The effect of this method is very good. If you Debug several times, you will deepen your understanding of the use, code writing, and this technology, and you will suddenly realize some confusion in the past.

In the process of debugging, you should find a context. Don't be anxious. Take more notes in the early stage and don't check it. Everything will be very easy.

Hello, I'm blogger Ning Zaichun. Let's cheer together!!!

Previously: 👉 SpringBoot integrates Security to realize permission control

This article is suitable for those who need to get started and who already know how to use Security simply.

For a technology, being able to use it means that we have a simple understanding of it and have a clear understanding of the context, so that we can better use it and better realize customization.

Next let's 😀 Let's take a look.

How Security handles form submission, account and password, and how to save user identity information.

If there are deficiencies, please criticize and correct.

I 🍟 Preface: flow chart:

II 🍤 Foreground send request

The user submits the user name and password to the / login interface by POST/ Login is the default interface when it is not specified

III 🧀 The request reached the UsernamePasswordAuthenticationFilter filter

The request first comes: 👉 UsernamePasswordAuthenticationFilter

/**
UsernamePasswordAuthenticationFilter: Process authentication form submission
 And encapsulating the request information as Authentication and returning it to the upper parent class,
The parent class passes SecurityContextHolder.getContext().setAuthentication(authResult); Save the authenticated Authentication to the security context
 */
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");

    //It can be modified through the corresponding set method
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;

       //  Initialize a user password authentication filter. The default login uri is / login. The request method is POST
	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
       	//Encapsulate the account name and password into an authentication Token object, which is a pass. However, the status at this time is not trusted and will become trusted only after passing the authentication
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
        //Record the remote address and set the session ID if the session already exists (it will not be created)
		setDetails(request, authRequest);
        //Use the AuthenticationManager in the parent class to authenticate the Token 
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/**
		obtainUsername And obtainPassword are convenient to obtain username and password from request
		In fact, most of us can't use it in projects where the front and rear ends are separated 😂   Because the front end transmits JSON data, we usually use JSON tool classes for parsing
	 */
	@Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(this.passwordParameter);
	}
	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

	/**
		Provides details that subclasses can configure to put in authentication requests
	 */
	protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
	}
	
    /**
	...Omit some unimportant code set get
	*/
}

IV 🍹 Create UsernamePasswordAuthenticationToken

Make the obtained data into a token UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

As we mentioned in the figure before, what we actually encapsulate is an Authentication object, and UsernamePasswordAuthenticationToken is a default implementation class.

Let's take a brief look at their structure diagram:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // Here is the user name and password to be rewritten according to your own needs
	private final Object principal;
	private Object credentials;

	/**
//Encapsulate the account name and password into an authenticated UsernamePasswordAuthenticationToken object. This is a pass, but the status is not trusted at this time,
//We can also see that the permission is null and setAuthenticated(false); Yes means that the identity is unauthenticated at the moment, so the state is not trusted at this time
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/**	This is the credible state */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
	// ...
}

It is currently in an unauthorized state. What we need to do later is to authenticate and authorize it.

V 🍰 The AuthenticationManager in the parent class authenticates the Token

AuthenticationManager is the core interface of authentication

We continue to return this.getAuthenticationManager().authenticate(authRequest); Conduct analysis

//We can see that AuthenticationManager is actually an interface, so it does not do real things, but provides a standard. Let's continue to look at its implementation class and see who helped it do things.
public interface AuthenticationManager {
    //An attempt is made to authenticate the passed Authentication object. If successful, a fully populated Authentication object (including the granted permissions) is returned.
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

Vi 🥑 We found the AuthenticationManager implementation class ProviderManager

We found ProviderManager and implemented AuthenticationManager. (but you'll find that it doesn't do anything and gives it to others 😂)

The ProviderManager does not directly authenticate the request, but delegates it to a list of authenticationproviders. Each AuthenticationProvider in the list will be queried in turn for whether it needs to be authenticated. The Authentication results of each provider are only two cases: throwing an exception or completely filling in all the attributes of an Authentication object .

In this reading, I deleted many miscellaneous codes, some judgments and exception handling, and only looked at the most important ones.

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    //Some code is omitted
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
	/**
	 * An attempt was made to authenticate the passed Authentication object. The list of authenticationproviders will be tried continuously,
	 * Until the AuthenticationProvider indicates that it can verify the type of Authentication object passed. Then it will try to use the AuthenticationProvider.
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
 
        //We traverse each Provider in the AuthenticationProvider list to authenticate in turn
       // However, you will find that the AuthenticationProvider is also an interface, and its implementation class is the real person who does things, as shown below
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			//...
			try {
                //provider.authenticate()
                //Parameter: authentication - authentication request object.
                //Return: a fully authenticated object, including credentials. If the AuthenticationProvider cannot support Authentication of the passed Authentication object, it may return null. Let's see what its implementation class looks like next
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
			//....
			}
		}
        // If all the providers in the AuthenticationProvider list fail to authenticate, and an AuthenticationManager implementation class has been constructed before, use the AuthenticationManager implementation class to continue authentication
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			// ...
			}
		}
         //Authentication successful
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				//Delete verification information after successful authentication
				((CredentialsContainer) result).eraseCredentials();
			}
            //Publish login success events
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}
		// If the authentication is not successful, an exception is thrown
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		prepareException(lastException, authentication);
		throw lastException;
	}
}

VII 🍦 AuthenticationProvider interface

public interface AuthenticationProvider {

	/**
	Authentication method
	Parameter: authentication - authentication request object.
	Return: a fully authenticated object, including credentials.
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	/**
		Does the Provider support the corresponding Authentication
		Returns true if this AuthenticationProvider supports the specified Authentication object.	
	 */
	boolean supports(Class<?> authentication);

}

Note: Boolean supports (class <? > authentication); The notes of the complete JavaDoc are:

If multiple authenticationproviders support the same Authentication object, the first Provder that can successfully verify Authentication will fill in its properties and return results, overwriting any possible authenticationexceptions thrown by earlier supported authenticationproviders. Once successfully verified, subsequent authenticationproviders will not be attempted. If all authenticationproviders fail to successfully verify Authentication, the AuthenticationException thrown by the last Provider will be thrown. (AuthenticationProvider can be configured in Spring Security configuration class)

Machine translation is not easy to understand. We translate it into easy to understand:

Of course, sometimes we have multiple different authenticationproviders that support different Authentication objects. When a specific AuthenticationProvider is passed into the ProviderManager, its corresponding supported provider will be selected in the AuthenticationProvider list to verify the corresponding Authentication object

This knowledge is related to the realization of multiple login methods. Let me briefly explain my understanding.

Here we explain the default login method, which uses UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationToken and DaoAuthenticationProvider later to verify identity. However, if we need to add SMS authentication code login, email authentication code or third-party login later.

Then we will inherit AbstractAuthenticationProcessingFilter, AbstractAuthenticationToken and AuthenticationProvider again for rewriting, because different login methods have different authentication logic and AuthenticationProvider. We log in with user name and password, Security provides a simple implementation of DaoAuthenticationProvider, which uses a UserDetailsService to query user name, password and GrantedAuthority. In actual use, we will implement the UserDetailsService interface to query relevant user information from the database, The authentication core of the AuthenticationProvider is to load the corresponding UserDetails to check whether the password entered by the user matches it.

The flow chart is roughly as follows:

The above figure is from: https://juejin.cn/post/6854573219936993287#heading-4

VIII 🍭 DaoAuthenticationProvider

AuthenticationProvider has many implementation classes and inheritance classes. If we look directly at the User related, we will first find the abstract class AbstractUserDetailsAuthenticationProvider.

Let's first look at this abstract class, and then look at its implementation classes to see how they go step by step.

/**
A basic AuthenticationProvider that allows subclasses to override and use UserDetails objects. This class is designed to respond to UsernamePasswordAuthenticationToken authentication requests.
After successful authentication, a UsernamePasswordAuthenticationToken is created and returned to the caller. The token takes as its principal a String representation of the user name or UserDetails returned from the authentication repository.
 */
public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    //... omitted some code
	private UserCache userCache = new NullUserCache();

	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    
	//Authentication method
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
        //Judge whether the user name is empty
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
        //Check the cache first
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
            //Some checks
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
            //retrieveUser is a method without abstraction. We'll see how its implementation class is implemented later
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            //Check whether some information is available to users or not
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        //Create a successful Authentication object.
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	private String determineUsername(Authentication authentication) {
		return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
	}

	/**
		Create a successful Authentication object. This also allows the implementation of classes.
		If you want to encrypt the password, the general word class will be re implemented
	 */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		//Identity information is also added here
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}



	/**
Allow subclasses to actually retrieve UserDetails from implementation specific locations. If the credentials provided are incorrect, you can choose to throw an AuthenticationException immediately (if you need to bind to the resource as a user to obtain or generate a UserDetails)
	 */
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;

    //...
}

DaoAuthenticationProvider: people who really do things

/**
AuthenticationProvider implementation that retrieves user details from UserDetailsService.
 */
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    // ... omitted some code
    
	/** */
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //UserDetailsService is simply an interface that loads the corresponding UserDetails (usually from the database), and UserDetails contains more detailed user information
            //Obtain user information through loadUserByUsername and return a UserDetails 
			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) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}   
	}
   	// Re the method of the parent class to encrypt the password
    @Override
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                         UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }

	//...
}

IX 🍣 UserDetailsService and UserDetails interfaces

UserDetailsService simply defines an interface to load the corresponding UserDetails. In use, most of us will implement this interface to query relevant user information from the database.

//The core interface for loading user specific data.
public interface UserDetailsService {
		//Locate users by user name
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetails is also an interface. In actual development, it will also be implemented and customized.

/**
Provide core user information.
For security purposes, Spring Security does not use the implementation directly. They simply store user information and then encapsulate it into an Authentication object. This allows non security related user information (e.g. e-mail address, phone number, etc.) to be stored in a convenient location.
 */
public interface UserDetails extends Serializable {
	//Returns the permissions granted to the user. 
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();

	//Indicates whether the user's account has expired. Failed to verify expired account
	boolean isAccountNonExpired();

	//Indicates whether the user is locked or unlocked. The locked user cannot be authenticated.
	boolean isAccountNonLocked();

	//Indicates whether the user's credentials (password) have expired. Expired credentials prevent authentication.
	boolean isCredentialsNonExpired();

	//Indicates whether the user is enabled or disabled. Unable to authenticate disabled users.
	boolean isEnabled();
}

10, 🍻 Return process

1. In the UserDetails retrieveUser() method under DaoAuthenticationProvider class, use this.getUserDetailsService().loadUserByUsername(username); After obtaining user information;

2. Return UserDetails to the caller in the parent class AbstractUserDetailsAuthenticationProvider (that is, in the authentication authenticate (authentication) method)

3. After AbstractUserDetailsAuthenticationProvider gets the returned UserDetails, the last thing returned to the caller is return createSuccessAuthentication(principalToReturn, authentication, user); Here, a trusted UsernamePasswordAuthenticationToken is created, that is, identity certificate.

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                     UserDetails user) {
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                                                                                         authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());
    this.logger.debug("Authenticated user");
    return result;
}

4. We go back to the call in the Authentication authenticate(Authentication authentication) method of ProviderManager. At this time, our user information has been verified, and we then return to the upper call.

5. Return to return this.getAuthenticationManager().authenticate(authRequest) in UsernamePasswordAuthenticationFilter; Statement, you have to continue to return to the upper layer at this time

6. Returning to the AbstractAuthenticationProcessingFilter, we directly press ctrl+b to see who called it.

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            // This is the calling place.
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            // session related. We won't talk about it here
            //Perform functions related to Http sessions when new authentication occurs.
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            //Look at the method name, we know this is what we need
            //Call after successful verification of province
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            //Validation failed call
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            //Validation failed call
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}
//Default behavior for successful authentication.
	//1. Successfully set Authentication object on SecurityContextHolder
	//2. Notifies the configured membermeservices that login succeeded
	//3. Trigger InteractiveAuthenticationSuccessEvent through the configured ApplicationEventPublisher
	//4. Delegate additional behavior to AuthenticationSuccessHandler.
//Subclasses can override this method to continue FilterChain after successful authentication.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    //Save authenticated Authentication to security context
    SecurityContextHolder.getContext().setAuthentication(authResult);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

In fact, whether it is a successful call or a failed call, most of them need to be rewritten in actual use to return the data we want to return to the front end.

🚀 think aloud

The next article will write an article on using Security to realize multiple authentication methods. There will be process analysis, source code, sql and blog. It should be done in these two days.

You can pay attention to it first, and continuously update various back-end technology blogs. If you have any questions, you are welcome to communicate with us!!!

Topics: Java Spring Boot Spring Security