Original blog address: pjmike’s blog

preface

The basic components of Spring Security were introduced in the last article. With this foundation in mind, this article will analyze the authentication process of Spring Security in detail.

One of the cores of Spring Security is its filter chain. Let’s start with its filter chain. The following is an execution process of Spring Security filter chain.

Introduction to the core filter chain

There are many filters in Spring Security. A typical project has a dozen or so filters, sometimes including custom filters. Of course, it is not possible to analyze every filter. Here are some core filters:

  • SecurityContextPersistenceFilter: the beginning of the entire Spring Security filter chain, which serves two purposes: one is to check when a request comes inSessionExists inSecurityContextIf it doesn’t exist, create a new oneSecurityContext. The second is when the request endsSecurityContextIn theSession, and clear itSecurityContextHolder.
  • UsernamePasswordAuthenticationFilter: inherits from abstract classesAbstractAuthenticationProcessingFilterFor form login, the Filter encapsulates the user name and password into oneUsernamePasswordAuthenticationVerify.
  • AnonymousAuthenticationFilter: Anonymous identity Filter. If the current Filter still has no user information after authentication, the Filter generates an anonymous identityAnonymousAuthenticationToken. The general function is for anonymous login.
  • ExceptionTranslationFilter: exception conversion filter for processingFilterSecurityInterceptorThe exception thrown.
  • FilterSecurityInterceptor: filter chain last checkpoint, get the Authentication from SecurityContextHolder, than in a user has permissions and access to the resources needed.

Form login authentication process

When accessing a protected resource, if login authentication has not been performed before, the system will return a login form or a response that prompts us to login first. Our analysis here is only for form logins, so we first fill in the form with a user name and password for login authentication.

The above has introduced a heap of filter core, here from SecurityContextPersistenceFilter the beginning of the filter to analyze the entire form login authentication process.

SecurityContextPersistenceFilter

When we fill out the form has been completed, click the login button, request after SecurityContextPersistenceFilter Filter, first mentioned in front, the Filter has two functions, one of which is in the request arrival, create SecurityContext security context, Let’s take a look at how it is done inside, part of the source code is as follows:

public class SecurityContextPersistenceFilter extends GenericFilterBean {

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";
    // The repository for security context storage
	private SecurityContextRepository repo;

	private boolean forceEagerSessionCreation = false;

	public SecurityContextPersistenceFilter(a) {
	    // Use HttpSession to store the SecurityContext
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
        // If it is the first request, there must be no FILTER_APPLIED attribute in the request
		if(request.getAttribute(FILTER_APPLIED) ! =null) {
			// Ensure that filters are applied only once per request
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();
        // Set the FILTER_APPLIED attribute to true in the request, so that the same request can be accessed again and the subsequent Filter operation can proceed directly
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        
		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: "+ session.getId()); }}// Encapsulate requset and response
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		// loads the SecurityContext SecurityContext from the repository storing the SecurityContext, which internally gets the context information from the Session
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
		    // The security context information is set to the SecurityContextHolder so that subsequent calls to the SecurityContextHolder in the same thread can retrieve the SecuritContext
			SecurityContextHolder.setContext(contextBeforeChainExecution);
            // Go to the next filter operation
			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
		    // After the request ends, clear the security context information
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			// Stores the security context information in the Session, which is equivalent to the maintenance of the login state
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed"); }}}public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
		this.forceEagerSessionCreation = forceEagerSessionCreation; }}Copy the code

The request arrives, the use of HttpSessionSecurityContextRepository read security context. Set the security context to the SecurityContextHolder and proceed to the next filter.

At the end of the request, using the same HttpSessionSecurityContextRepository this security context of storage warehouse SecurityContext after certification to be included in the Session, which is the key to login state maintenance, concrete operating here did not elaborate.

UsernamePasswordAuthenticationFilter

After SecurityContextPersistenceFilter filter later to UsernamePasswordAuthenticationFilter filter, because we assume that the request for the first time, So the SecurityContext does not contain authenticated Authentication. The actions from this filter are critical for form logins and contain the core authentication steps for form logins. Here is a diagram of the authentication process in this filter:

UsernamePasswordAuthenticationFilter is the parent of the AbstractAuthenticationProcessingFilter, first into the parent class foFilter method, part of the source code is as follows:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	...
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; . Authentication authResult; Try {/ / call the subclasses UsernamePasswordAuthenticationFilter attemptAuthentication method authResult = attemptAuthentication (request,  response);if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed // authentication // Subclass completed authentication, return immediately; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } the catch (AuthenticationException failed) {/ / authentication failed unsuccessfulAuthentication (request, response, failed); return; } / / certification success if (continueChainBeforeSuccessfulAuthentication) {/ / continue calling the next Filter chain. The doFilter (request, response); } // Write successfulAuthentication(Request, Response, chain, authResult) to SecurityContext; }}Copy the code

A core in the doFilter method is called a subclass UsernamePasswordAuthenticationFilter attemptAuthentication method, this method into the real certification process, And return Authentication after Authentication, the source code of this method is as follows:

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		// It must be a POST request
		if(postOnly && ! request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
        // Get the user name and password in the form
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();
        / / user name and password are encapsulated into a UsernamePasswordAuthenticationToken
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        // The core part is handed over to the internal AuthenticationManager for Authentication and returns authenticated Authentication
		return this.getAuthenticationManager().authenticate(authRequest);
	}
Copy the code

. This method is one of the key point is his getAuthenticationManager () authenticate (authRequest), called inside the AuthenticationManager to certification, The AuthenticationManager, introduced in a previous article, is the core interface for identity authentication. Its implementation class is ProviderManager, which delegates requests 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

To analyze a key AuthenticationProvider below, it is DaoAuthenticationProvider, it was the original provider of framework, also is the most commonly used the provider. In most cases we will rely on it for identity authentication, it is the parent of the AbstractUserDetailsAuthenticationProvider, certification process will first invoke the parent class authenticate method, core source code is as follows:

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports"."Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null)?"NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
			    1 / / call subclasses DaoAuthenticationProvider retrieveUser populated UserDetails () method
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			// If UserDetails is not given, an exception will be thrown
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
		    2 // Some properties of UserDetails are pre-checked to determine whether the user is locked, available, and expired
			preAuthenticationChecks.check(user);
			3 // Additional checks on UserDetails to match the password of the incoming Authentication with that of the UserDetails retrieved from the database
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throwexception; }}4 // Post-check UserDetails to check whether the password of UserDetails has expired
		postAuthenticationChecks.check(user);

		if(! cacheWasUsed) {this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        5 // After all the above checks are successful, a successful Authentication is generated using the passed user information and the obtained UserDetails
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
Copy the code

From the above large string of source code, extract a few key methods:

  • retrieveUser(…) : call subclasses DaoAuthenticationProvider retrieveUser populated UserDetails () method
  • PreAuthenticationChecks (user) : the preliminary inspections populated UserDetails was obtained from the above, namely to determine whether a user locks, availability and user is late
  • AdditionalAuthenticationChecks (user authentication) : to examine the populated UserDetails additional, authentication and access to populated UserDetails of incoming passwords match
  • PostAuthenticationChecks. Check (user) : after inspections populated UserDetails, namely check whether populated UserDetails password expired
  • CreateSuccessAuthentication (principalToReturn, authentication, user) : After all the above checks are successful, a successful Authentication is generated using the incoming Authentication and the obtained UserDetails

retrieveUser(…) methods

Let’s talk more about retrieveUser(…) Methods, DaoAuthenticationProvider retrieveUser () the source code is as follows:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	prepareTimingAttackProtection();
	try {
	    // The UserDetailsService is used to obtain the 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); }}Copy the code

The core part of this method is to call the internal UserDetailsServices to load UserDetails. UserDetailsServices is essentially the interface for loading UserDetails. UserDetails contains more detailed user information than Authentication. UserDetailsService common implementation classes are JdbcDaoImpl InMemoryUserDetailsManager, the former is loaded from the database user, the latter from memory load users. We can also implement the UserDetailsServices interface ourselves. For example, if we are doing database based authentication, we can implement this interface manually instead of using JdbcDaoImpl.

additionalAuthenticationChecks()

The pre-check and post-check for UserDetails are simple, so we won’t go into details here. Let’s look at the password match verification code as follows:

protected void additionalAuthenticationChecks(UserDetails 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();
        // Use PasswordEncoder to verify the password
		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")); }}Copy the code

This method is actually call the DaoAuthenticationProvider additionalAuthenticationChecks method, internal calls the encryption password decryption (CCM) match, if the match fails, Throws a BadCredentialsException

Finally through the createSuccessAuthentication (..) The getUserDetails () method generates a successful Authentication. In short, it combines the obtained UserDetails with the passed Authentication to get a fully populated Authentication.

The Authentication finally back up, step by step to the AbstractAuthenticationProcessingFilter filter, it is set to SecurityContextHolder.

AnonymousAuthenticationFilter

Anonymous authentication Filter, it is mainly for anonymous login, if the previous Filter, such as UsernamePasswordAuthenticationFilter after the execution, SecurityContext still no user information, So AnonymousAuthenticationFilter to be effective, generate an anonymous identity information – AnonymousAuthenticationToken

ExceptionTranslationFilter

ExceptionTranslationFilter FilterSecurityInterceptor simple say is processing the exception thrown, its internal doFilter method source code is as follows:

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
		    // Go directly to the next Filter
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		// This is where the real action is, handling the exception thrown
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            / / here will process FilterSecurityInterceptor AccessDeniedException thrown
			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if(ase ! =null) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw newRuntimeException(ex); }}}Copy the code

FilterSecurityInterceptor

FilterSecurityInterceptor filter is the last checkpoint, request will eventually come here before, its working process is roughly

  • Encapsulate request information
  • Read configuration information, that is, permission information required by resources, from the system
  • fromSecurityContextHolderTo obtain the previously authenticatedAuthenticationObject, which represents the permissions of the current user
  • Then, based on the three kinds of information obtained above, it is passed into a permission verifier, which compares the permissions that the user has with the permissions required for the resource for the current request. If the comparison is successful, the request processing logic of the real system will be entered, otherwise, the corresponding exception will be thrown

Draw a simple diagram below to illustrate FilterSecurityInterceptor execution, as follows:

According to the above content, we take a look at FilterSecurityInterceptor source,

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {
	 ...
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// Encapsulate request, Response request FilterInvocationfi= new FilterInvocation(request, response, chain); // Invoke the core method (fi); }... public void invoke(FilterInvocationfi) throws IOException, ServletException {
	if((fi.getRequest() ! = null) && (fi.getRequest().getAttribute(FILTER_APPLIED) ! = null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else {// If (fi.getrequest ()! = null && observeOncePerRequest) {// If the current request has already passed the security filter judgment, no further logic will be executed and the request will proceed directly, Call request handler fi.getrequest ().setattribute (FILTER_APPLIED, Boiler.true); } // Call the parent method, perform authorization judgment logic InterceptorStatusToken Token = super.beforeInvocation(FI); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); }}}Copy the code

The request is encapsulated in the source code, and then goes to the core, calling the parent’s authorization method — beforeInvocation(FilterInvocation), source code as follows:

protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();

		if(! getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException(
					"Security invocation attempted for object "
							+ object.getClass().getName()
							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
							+ getSecureObjectClass());
		}
		// Read the Configuration information for Spring Security and encapsulate it as ConfigAttribute
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);
		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'"); }...return null; // no further work post-invocation}...if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound"."An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}
		// Get Authentication from SecurityContextHolder
		Authentication authenticated = authenticateIfRequired();

		// Start authorization matching
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throwaccessDeniedException; }... }Copy the code

The beforeInvocation source code is abundant and I have kept only the core part of it here. From the source code, once the configuration and user information is available, The request information is passed into the AccessDecisionManager’s Decide (Authentication Authentication, Object Object,Collection

configAttributes) method. This method is where the authorization validation logic is ultimately performed.

The AccessDecisionManager itself is an interface, it is AbstractAccessDecisionManager implementation classes, and AbstractAccessDecisionManager is an abstract class, its implementation class three, AffirmativeBased is commonly used, and the final authorization verification logic is realized by AffirmativeBased. Part of the source code is as follows:

public void decide(Authentication authentication, Object object, Collection
       
         configAttributes)
        throws AccessDeniedException {
	int deny = 0;
    // The voting machine performs the voting
	for (AccessDecisionVoter voter : getDecisionVoters()) {
		intresult = voter.vote(authentication, object, configAttributes); .switch (result) {
		case AccessDecisionVoter.ACCESS_GRANTED:
			return;

		case AccessDecisionVoter.ACCESS_DENIED:
			deny++;

			break;

		default:
			break; }}if (deny > 0) {
		throw new AccessDeniedException(messages.getMessage(
				"AbstractAccessDecisionManager.accessDenied"."Access is denied")); }... }Copy the code

The simple logic of this method is to execute the verification logic of AccessDecisionVoter. If the verification fails, an AccessDeniedException is thrown. The AccessDecisionVoter vote logic will not be discussed here. After Spring Security 3.0, By default, WebExpressionVoter, the implementation class of the AccessDecisionVoter interface, is used to complete the final verification process.

summary

The above provides a fairly detailed analysis of Spring Security’s authentication process from the perspective of filters, but of course there are many details that are not covered.

References & acknowledgements

  • Spring Security(4)- Source code analysis of core filters
  • SPRING SECURITY 4 official documentation Chinese translation and source code interpretation
  • Spring Security