preface

I read too much about Spring Seuciry, but I still feel that my understanding is not solid enough, and I still need to rely on knowledge output to consolidate.

Filter chain and certification process

An authentication process is actually performed by a green rectangle Filter on the Filter chain.

The basic certification process has three steps:

  1. Filter intercepts the request and generates an unauthenticatedAuthenticationUnder theAuthenticationManagerConduct certification;
  2. AuthenticationManagerDefault implementation ofProviderManagerthroughAuthenticationProviderrightAuthenticationCertification, its own do not do certification processing;
  3. If the authentication succeeds, create an authenticated oneAuthenticationReturn; Otherwise, an exception is thrown to indicate that the authentication fails.

To understand the process, from class UsernamePasswordAuthenticationFilter, ProviderManager, DaoAuthenticationProvider and InMemoryUserDetailsManager (UserDetailsService implementation class, Provided by the default configuration UserDetailsServiceAutoConfiguration) to understand. Just create a Springboot project with spring-boot-starter-Security and set the breakpoint interface appropriately to see the process.

Explain with the certification department

)

After the request is sent to the front desk, the front desk responsible for the request will encapsulate the content of the request as an Authentication object and hand it to the Authentication management department. The Authentication management department only manages the Authentication department and does not do specific Authentication operations. The specific operations will be processed by the Authentication department related to the front desk. Of course, each Authentication department needs to determine whether the Authentication department is responsible for the Authentication, if yes, the department is responsible for the Authentication, otherwise the next department is responsible for the Authentication. After the Authentication succeeds, the Authentication department will create a successful Authentication return. Otherwise, an exception is thrown to indicate that the authentication fails, or the authentication is handled by the next department.

If you need to add an authentication type, you only need to add the corresponding foreground (Filter) and the AuthenticationProvider corresponding to the foreground (Filter). Of course, you can also add an authentication department corresponding to the existing foreground. The Authentication department will use the authentication-generated front end to determine whether it is responsible for the Authentication, and thus may provide a mutually agreeable Authentication.

When the certification department needs personnel information, it can obtain it from the personnel information Department. Different systems have different personnel information departments, we need to provide the personnel information department, otherwise we will get blank files. Of course, personnel data department is not necessarily unique, certification department can have its own exclusive data department.

The figure above can also be drawn as follows:

This might be more like FilterChain. Each foreground is actually one in the FilterChain. The customer requests authentication one by one with the request. After finding the correct foreground, the authentication judgment is made.

Foreground (Filter)

The foreground Filter here only refers to the Filter that implements authentication. The Spring Security Filter Chain processes these filters and other filters, such as CsrfFilter. If you have to give characters to them, think of them as security personnel.

Spring Security provides us with three implemented filters. UsernamePasswordAuthenticationFilter BasicAuthenticationFilter and RememberMeAuthenticationFilter. If you don’t do any configuration, personalized UsernamePasswordAuthenticationFilter and BasicAuthenticationFilter in default in the filter chain. The two authentication modes are the default authentication modes.

UsernamePasswordAuthenticationFilter will merely to/login path effect, that is to say UsernamePasswordAuthenticationFilter is responsible for the certification, issued certification interface for/login.

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {...public UsernamePasswordAuthenticationFilter(a) {
		super(new AntPathRequestMatcher("/login"."POST")); }... }Copy the code

UsernamePasswordAuthenticationFilter as abstract class AbstractAuthenticationProcessingFilter an implementation, BasicAuthenticationFilter for abstract class BasicAuthenticationFilter an implementation. The source code of these four classes provides good foreground (Filter) implementation ideas.

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter provides certification needs to be done before and after, All subclasses need to attemptAuthentication(HttpServletRequest, HttpServletResponse) that implements the abstract method to complete the certification. When using AbstractAuthenticationProcessingFilter, there is a need to provide a intercept path (using AntPathMatcher matching) to intercept the corresponding specific path.

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter as actual at the front desk, Will the client to submit the username and password in a UsernamePasswordAuthenticationToken to certification management department (the AuthenticationManager) certification. Her job was done.

BasicAuthenticationFilter the front desk (Filter) will only be dealt with Authorization Header, and lowercase values after starting with the basic request, otherwise, the front desk (Filter) is not responsible for processing. The Filter retrieves the Base64 encoded username and password from the header, Create UsernamePasswordAuthenticationToken provide authentication management department (AuthenticationMananager) certification.

Authentication Information

After receiving the request, the front desk will obtain the required information from the request and create the Authentication information recognized by its own Authentication Department (AuthenticationProvider). The AuthenticationProvider determines whether the Authentication data is processed by the Authentication provider.

public interface Authentication extends Principal.Serializable {
	
	// Permissions that the principal has. The AuthorityUtils utility class provides some handy methods.
	Collection<? extends GrantedAuthority> getAuthorities();
	// A certificate proving the Principal's identity, such as a password.
	Object getCredentials(a);
	// Additional information of the authentication request, such as IP.
	Object getDetails(a);
	// Party. In username+password mode, it is username, or userDetails after userDetails.
	Object getPrincipal(a);
	// Whether the authentication has been passed.
	boolean isAuthenticated(a);
	// Set to pass authentication.
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Copy the code

After Authentication is authenticated, it is stored in a Thread-local SecurityContext.

/ / set
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
/ / to get
Authentication existingAuth = SecurityContextHolder.getContext()
				.getAuthentication();
Copy the code

When writing the foreground of the Filter, you can check SecurityContextHolder. GetContext () in the existing Authentication through the Authentication, if so, then you can skip this Filter. You are advised to set the authenticated Authentication instance to an unmodifiable instance.

The class diagram of the Authentication into the implementation class, for the Authentication of abstract subclass AbstractAuthenticationToken implementation class. Implementation class has several, related to the talk of the Filter in front of a UsernamePasswordAuthenticationToken and RememberMeAuthenticationToken.

A subclass of AbstractAuthenticationToken CredentialsContainer and Authentication. Some simple methods are implemented, but the main ones still need to be implemented. The implementation of the getName() method of this class can see that the common principal classes are UserDetails, AuthenticationPrincipal, and Princial. If you need to set an object to Principal, consider inheriting one of these three classes.

public String getName(a) {
	if (this.getPrincipal() instanceof UserDetails) {
		return ((UserDetails) this.getPrincipal()).getUsername();
	}
	if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
		return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
	}
	if (this.getPrincipal() instanceof Principal) {
		return ((Principal) this.getPrincipal()).getName();
	}

	return (this.getPrincipal() == null)?"" : this.getPrincipal().toString();
}
Copy the code

AuthenticationManager

AuthenticationManager is an interface that authenticates Authentication. If authenticated, the returned Authentication should be accompanied by the GrantedAuthority that the principal has.

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}
Copy the code

The interface’s comments state that exceptions must be checked and thrown in the following order:

  1. DisabledException: The account is unavailable
  2. LockedException: The account is locked
  3. BadCredentialsException: The certificate is incorrect

Spring Security provides a default implementation, ProviderManager. The ProviderManager only performs the management function, and the authentication function is performed by the AuthenticationProvider.

public class ProviderManager implements AuthenticationManager.MessageSourceAware.InitializingBean {...public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

	public ProviderManager(List
       
         providers, AuthenticationManager parent)
        {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			// #1 to check whether 'AuthenticationProvider' is authenticated by the authentication department
			if(! provider.supports(toTest)) {continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				// #2, the authentication department performs the authentication
				result = provider.authenticate(authentication);

				if(result ! =null) {
					copyDetails(authentication, result);
					// #3, the next authentication department will not be authenticated, otherwise the exception thrown will be caught, execute the next authentication department (AuthenticationProvider)
					break; }}catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch(AuthenticationException e) { lastException = e; }}if (result == null&& parent ! =null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch(AuthenticationException e) { lastException = parentException = e; }}// #4, if the authentication is successful, perform the operation after the authentication is successful
		if(result ! =null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		// #5, if the authentication fails, an exception must be thrown, otherwise, the corresponding authentication department is not configured (AuthenticationProvider)
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound".new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throwlastException; }... }Copy the code
  1. Go through all the authentication departments (AuthenticationProvider) and find the supported authentication department for authentication
  2. Certification is conducted by the certification department
  3. If the authentication passes, the next authentication department will not authenticate. Otherwise, the thrown exception will be caught and the next authentication department (AuthenticationProvider) will be executed.
  4. If the authentication succeeds, perform the operations after the authentication succeeds
  5. If the authentication fails, an exception must be thrown. Otherwise, the corresponding authentication department (AuthenticationProvider) is not configured.

When using the Spring Security OAuth2, OAuth2AuthenticationManager will see another implementation.

Authentication Department (AuthenticationProvider)

The AuthenticationProvider is responsible for the actual authentication and works in conjunction with the authentication manager. Perhaps other authentication managers do not need the cooperation of the AuthenticationProvider.

public interface AuthenticationProvider {
	// Perform authentication
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	// Whether to authenticate by the AuthenticationProvider
	boolean supports(Class
        authentication);
}
Copy the code

The interface has a number of implementation classes, Which contains the RememberMeAuthenticationProvider (AuthenticationProvider directly) and DaoAuthenticationProvider (through AbastractUserDetailsAuthenti CationProvider introduction inheritance). Here the key about AbastractUserDetailsAuthenticationProvider and DaoAuthenticationProvider.

AbastractUserDetailsAuthenticationProvider

As the name implies, AbastractUserDetailsAuthenticationProvider is populated UserDetails support Provider, other Provider, Such as RememberMeAuthenticationProvider do not need to use populated UserDetails. The abstract class has two abstract methods that the implementation class needs to do:

/ / get populated UserDetails
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException;

protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException;
Copy the code

The retrieveUser() method provides the UserDetails for the validation. Let’s take a look at UserDetails:

public interface UserDetails extends Serializable {		Collection<? extends GrantedAuthority> getAuthorities();	String getPassword(a);		String getUsername(a);	// Whether the account is expired Boolean isAccountNonExpired(); // Whether the account is locked Boolean isAccountNonLocked(); // Whether the certificate (password) expires Boolean isCredentialsNonExpired(); // Whether the account is available Boolean isEnabled(); }
Copy the code

AbastractUserDetailsAuthenticationProvider# authentication (authentication) is divided into three steps to verify:

  1. preAuthenticationChecks.check(user);
  2. additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
  3. postAuthenticationChecks.check(user);

PreAuthenticationChecks default implementation for DefaultPreAuthenticationChecks, responsible for checking:

  1. UserDetails#isAccountNonLocked()
  2. UserDetails#isEnabled()
  3. UserDetails#isAccountNonExpired()

PostAuthenticationChecks default implementation for DefaultPostAuthenticationChecks, responsible for checking:

  1. UserDetails#user.isCredentialsNonExpired()

AdditionalAuthenticationChecks needs to be done by the implementation class.

Check after successful AbstractUserDetailsAuthenticationProvider creates and returns a certified Authentication.

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {	// Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
Copy the code

DaoAuthenticationProvider

The following for DaoAuthenticationProvider implementation of AbstractUserDetailsAuthenticationProvider abstract methods.

/ / check if the password correctly protected void additionalAuthenticationChecks (populated UserDetails populated UserDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials()  == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (! passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }}// Get the UserDetails object protected Final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }... }
Copy the code

In the above code, you need to provide UserDetailsService and PasswordEncoder instances. Just instantiate the two classes and put them into the Spring container.

Data Department (UserDetailsService)

UserDetailsService interfaces provide the necessary certification process populated UserDetails classes, such as DaoAuthenticationProvider need a UserDetailsService instance.

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code

The realization of the Spring Security provides two UserDetailsService: InMemoryUserDetailsManager and JdbcUserDetailsManager. InMemoryUserDetailsManager as the default configuration, can be seen from UserDetailsServiceAutoConfiguration configuration. Of course, it is not easy to understand that a database-based implementation requires additional database configuration and is not suitable for default implementation. These two classes are the implementation classes of UserDetailsManager, which defines the CRUD operation of UserDetails. InMemoryUserDetailsManager using Map < String, MutableUserDetails > for storage.

public interface UserDetailsManager extends UserDetailsService {
	void createUser(UserDetails user);

	void updateUser(UserDetails user);

	void deleteUser(String username);

	void changePassword(String oldPassword, String newPassword);

	boolean userExists(String username);
}
Copy the code

If we need to add a UserDetailsService, we can implement UserDetailsService or UserDetailsManager.

Add a certification process

At this point, we know the flow of Spring Security. It can be seen from the above content that if you want to add a new authentication mode, you only need to add a combination of Filter (Foreground) + AuthenticationProvider (AuthenticationProvider) + Data room (UserDetailsService). In fact, the UserDetailsService is not required and can be implemented according to the requirements of the AuthenticationProvider.

I will explain this in another article using the example of mobile phone number + verification code login.