Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Recently, when I used this framework to write my graduation project, my friend asked me for a variety of login methods, saying that it is not good to log in only with account number and password, and asked me to add several methods, such as: SMS login, email login, third-party login, etc. (the first two have been implemented, third-party login is not fixed) at the beginning of the confusing, I don’t know how to start.

I read several blogs, but they were incomplete or too advanced for me. After that is to see the blog, said to understand the principle, process, writing a variety of ways is actually quite simple. Then I went to Debug honestly.

This effect is very good, more Debug a few times, no matter for use, or for writing code, as well as the understanding of this technology will deepen some, some confusion before will suddenly understand.

The Debug process should find a vein, don’t be impatient, take more notes in the early stage, not more check, so everything will be very easy.

Hello, I am ning Zaichun, blogger, let’s cheer up!!

Previous: 👉SpringBoot integrates Security and implements permission control

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

For a technology, the ability to use means that we have a simple understanding of it, the context is clear, we can better use it, and better achieve customization.

Let’s take a look at 😀.

How Security handles form submission accounts and passwords, and saves user identity information.

If there are shortcomings, please criticize and correct.

I. 🍟 Preface: Flow chart:

Two, 🍤 front desk send request

The user submits the user name and password to the /login interface in POST mode. /login is the default interface if not specified

Third, 🧀 request arrives at UsernamePasswordAuthenticationFilter filter

The request will come: first 👉 UsernamePasswordAuthenticationFilter

/ * * UsernamePasswordAuthenticationFilter: Handling Authentication form submission and will request information encapsulation for Authentication and then returned to the parent class at the top, the parent class through SecurityContextHolder. GetContext () setAuthentication (authResult); Save 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");

    // This can be changed using 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 mode is POST
	public UsernamePasswordAuthenticationFilter(a) {
		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, this state is not trusted, and will become trusted only after passing authentication
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
        // Records the remote address, and if the session already exists (it will not be created), the session ID will also be set
		setDetails(request, authRequest);
        // Use AuthenticationManager in the parent class to authenticate the Token
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/** obtainUsername and obtainPassword are convenient to obtainUsername and password from request. We usually use JSON utility 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 so that subclasses can configure to put authentication requests */
	protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
	}
	
    / * *... Omit some unimportant code set get */
}
Copy the code

Fourth, make UsernamePasswordAuthenticationToken 🍹

To obtain the data to make into a token UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken (username, password);

Before us in the picture about our actual encapsulation is an Authentication object, UsernamePasswordAuthenticationToken is a default implementation class.

Let’s take a quick look at their structure:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // Here is the user name and password customization according to their own needs to rewrite
	private final Object principal;
	private Object credentials;

	/ * * / / the account name, password, wrapped in a certification UsernamePasswordAuthenticationToken object, it is a pass, this state is not credible, however, / / in which we can see is null,  setAuthenticated(false); Is to indicate that the identity is not authenticated and therefore the state is not trusted */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/** this is the trusted state */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection
        authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
	// ...
}

Copy the code

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


The AuthenticationManager in 🍰 authenticates the Token

==AuthenticationManager is the identity authenticator and the core interface of authentication ==

We continue to return this. GetAuthenticationManager () authenticate (authRequest); Analyze.

// We can see that AuthenticationManager is really just an interface, so it doesn't really do anything but provide a standard, so let's go ahead and look at its implementation class to see who does the work for it.
public interface AuthenticationManager {
    // Try to authenticate the passed Authentication object, and return the fully populated Authentication object (including the permissions granted) if successful.
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
Copy the code

🥑 We found the AuthenticationManager implementation class ProviderManager

We find ProviderManager implements AuthenticationManager. (But you will find that it also does not work, and given to others to do 😂)

Rather than authenticating the request directly itself, the ProviderManager delegates it to a list of AuthenticationProviders. Each AuthenticationProvider in the list will be queried in turn to see if it needs to be authenticated. Each provider can only result in two cases: throwing an exception or completely filling all the properties of an Authentication object.

In this reading, I removed a lot of the miscellaneous code, some of the judgments, exception handling, I got rid of it, and just looked at the most important ones.

public class ProviderManager implements AuthenticationManager.MessageSourceAware.InitializingBean {

    // Omitted some code
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
	/** * Attempts to authenticate the passed Authentication object. The list of AuthenticationProviders will be continuously tried * until the AuthenticationProvider indicates that it can verify the type of Authentication object being passed. You will then 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 iterate through the AuthenticationProvider list for each Provider in turn
       // However, you will find that AuthenticationProvider is also an interface, and its implementation class is the one that does the real work, as described below
		for (AuthenticationProvider provider : getProviders()) {
			if(! provider.supports(toTest)) {continue;
			}
			/ /...
			try {
                //provider.authenticate()
                // Parameter: authentication - Authentication request object.
                // Returns: a fully authenticated object, including credentials. If the AuthenticationProvider does not support authenticating the passed Authentication object, it may return NULL. What does its implementation class look like
				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 was constructed previously, authentication continues with the AuthenticationManager implementation class
		if (result == null && this.parent ! =null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			// ...}}// The authentication succeeded
		if(result ! =null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				// Delete the authentication information after the authentication succeeds
				((CredentialsContainer) result).eraseCredentials();
			}
            // Publish a login success event
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}
		// Failed to authenticate, 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);
		throwlastException; }}Copy the code

🍦AuthenticationProvider interface

public interface AuthenticationProvider {

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

	/** Whether the Provider supports Authentication If the AuthenticationProvider supports the specified Authentication object, true is returned. * /
	boolean supports(Class
        authentication);

}
Copy the code

Boolean supports(Class
authentication); The full JavaDoc comments are:

If multiple AuthenticationProviders support the same Authentication object, the first Provder that successfully validates Authentication will populate its properties and return the result, To override any possible AuthenticationExceptions thrown by the AuthenticationProvider that was previously supported. Once the authentication is successful, subsequent AuthenticationProvider will not be attempted. If all authenticationProviders fail to verify Authentication successfully, the AuthenticationException thrown by the last Provider will be thrown. (AuthenticationProvider can be configured in the Spring Security configuration class.)

The machine translation is not very easy to understand, we translate it into simple point:

Of course, sometimes we have multiple AuthenticationProviders that support different Authentication objects, so when a specific AuthenticationProvier is passed inside the ProviderManager, It selects the supported provider from the AuthenticationProvider list to authenticate the corresponding Authentication object

This knowledge is related to implementing multiple login methods, so LET me briefly explain my understanding.

What we’re talking about here is the default login, Used is the text of the DaoAuthenticationProvider after UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationToken and these, for identity authentication, But if we need to add mobile phone SMS verification code login or email verification code or third party login and so on.

Then we will inherit AbstractAuthenticationProcessingFilter again, AbstractAuthenticationToken, AuthenticationProvider rewrite, Because the authentication logic is different in different login methods, AuthenticationProvider will also be different. We use the user name and password to log in. Security provides a simple implement AuthenticationProvider DaoAuthenticationProvider, It uses a UserDetailsService to query the user name, password and GrantedAuthority. In practice, we will implement the UserDetailsService interface to query relevant user information from the database. The core of AuthenticationProvider is to load the corresponding UserDetails to check whether the password entered by the user matches it.

The flowchart is roughly as follows:

The image above is from juejin.cn/post/685457…

Eight, 🍭 DaoAuthenticationProvider

AuthenticationProvider its implementation class, inheritance, class a lot, we are directly related to the User, will find AbstractUserDetailsAuthenticationProvider first the abstract class.

Let’s look at this abstract class first, and then at its implementation class, and see how they progress step by step.

/** A basic AuthenticationProvider that allows subclasses to override and use UserDetails objects. This class aims to response UsernamePasswordAuthenticationToken authentication request. After the success of the validation, will create UsernamePasswordAuthenticationToken and returns it to the caller. The token takes as its body the String representation including the user name or the UserDetails returned from the authentication repository. * /
public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider.InitializingBean.MessageSourceAware {

    / /... Some code has been omitted
	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"));
        // Check 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 that has no abstraction and we'll see how its implementation class is implemented later, okay
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            // Something to check whether the user is available 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 word classes to be implemented. If a password is to be encrypted, the general word class will be re-implemented */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
		// Identity information is 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;
	}



	/** allows subclasses to actually retrieve UserDetails from an implementation-specific location, with the option to throw an AuthenticationException immediately if the credentials provided are incorrect (if you need to bind to a resource as a user to get or generate a UserDetails) */
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;

    / /...
}

Copy the code

DaoAuthenticationProvider: real people to do things

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

    / /... Some code has been omitted
    
	/ * * * /
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            // The UserDetailsService is simply the interface to load the corresponding UserDetails (usually from a database), which contains more detailed user information
            // Get user information by 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 newInternalAuthenticationServiceException(ex.getMessage(), ex); }}// use the parent method 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);
    }

	/ /...
}
Copy the code

🍣UserDetailsService and UserDetails interface

The UserDetailsService simply defines an interface for loading the corresponding UserDetails. In use, most users can implement this interface to query the relevant user information from the database.

// The core interface to load user-specific data.
public interface UserDetailsService {
		// Locate the user based on the user name
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code

UserDetails is also an interface, which will also be implemented and customized for use in actual development.

/** Provide core user information. For Security purposes, Spring Security does not use implementations directly. They simply store user information and encapsulate that information into an Authentication object. This allows non-security-related user information (such as E-mail addresses, phone numbers, and so on) 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(a);
	String getUsername(a);

	// Indicates whether the user's account has expired. Unable to validate expired accounts
	boolean isAccountNonExpired(a);

	// indicates whether the user is locked or unlocked. The locked user could not be authenticated.
	boolean isAccountNonLocked(a);

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

	// indicates whether the user is enabled or disabled. Unable to authenticate a disabled user.
	boolean isEnabled(a);
}
Copy the code

10. 🍻 Return process

1, DaoAuthenticationProvider under populated UserDetails retrieveUser () method by enclosing getUserDetailsService () loadUserByUsername (username); After obtaining user information;

2, will be populated UserDetails returned to the parent class AbstractUserDetailsAuthenticationProvider call in place (i.e., the Authentication authenticate (Authentication Authentication)

3, after obtaining returns populated UserDetails AbstractUserDetailsAuthenticationProvider, Finally returned to the caller is the return createSuccessAuthentication (principalToReturn, authentication, user); Here is to create a credible UsernamePasswordAuthenticationToken, proof of identity.

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;
}
Copy the code

4. We go back to the invocation of the Authentication Authenticate (Authentication Authentication) method of ProviderManager. At this time, our user information has been authenticated. We then return to the upper call.

5, back to the return of UsernamePasswordAuthenticationFilter enclosing getAuthenticationManager () authenticate (authRequest); Statement, at this point you have to go back up

6, to return to the AbstractAuthenticationProcessingFilter, we directly press CTRL + b to see who is calling 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 where the call is made.
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            // Session is related. We won't go into that here
            // Perform Http session-related functions when new authentication occurs.
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            // This is the method we need to pull
            // called after successfully verifying the province
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            // Validate failed call
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            // Validate failed callunsuccessfulAuthentication(request, response, ex); }}}Copy the code
// The default behavior for successful authentication.
	// set the successful Authentication object on SecurityContextHolder
	//2, notify RememberMeServices configured successfully
	/ / 3 ApplicationEventPublisher trigger InteractiveAuthenticationSuccessEvent, through configuration
	/ / 4, will be entrusted to AuthenticationSuccessHandler additional behavior.
// 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 the authenticated Authentication to the 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);
}
Copy the code

In fact, whether the verification of successful call or failed call, most of our actual use, is to rewrite, we want to return to the front end of the data.

🚀 talk to yourself

The next article will write an article using Security to achieve a variety of authentication methods, process analysis, source code, SQL, blog will have, in these two days should be able to fix.

You can first point a concern oh, continue to update a variety of correct after the use of technology blog, what questions are welcome to communicate with you!!