Spring security source code learning

Based on the study of the source code, only part of the source code is discussed. The picture of Shang Silicon Valley teacher is used for reference, and the explanation of the source code is written in the code,
It is divided into three parts: authentication process, permission access process + spring security request to share authentication information

1. Certification process

It mainly relies on the filter: UsernamePasswordAuthenticationFilter, which is used for user login authentication and so on.
(1). The method AbstractAuthenticationProcessingFilter in the parent class is called

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {}

The filter depends on its doFilter, and the key points will be commented in the code

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {//1. Judge the submission method
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;//Class used to encapsulate validation information

		try {//2. Call attemptAuthentication in the subclass UsernamePasswordAuthenticationFilter to obtain the data of the form for authentication. After success, package the result into authResult
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
            //3. Configure session policies, such as setting the maximum concurrent number of sessions, etc
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
            //4 - 4.1 here means that if the authentication fails, the following method will be called
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
        //4-4.2 for the processing after successful authentication, continuechainbeforesuccessful authentication is false by default,
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}

		successfulAuthentication(request, response, chain, authResult);
	}

Process:
1. Judge the submission method
2. Call attemptAuthentication in the subclass UsernamePasswordAuthenticationFilter to obtain the data of the form for authentication. After success, package the result into authResult
3. Configure the session policy,
4
4.1 here, it means that if the authentication fails, the following method will be called
4.2 for the processing after successful authentication, continuechainbeforesuccessful authentication is false by default,

2. Go deep into the attemptAuthentication method in UsernamePasswordAuthenticationFilter:

Source code


The specific reason is the ghost of attemptAuthentication. The source code:

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {//1. Judge whether it is a post submission
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//If yes, get the user name and password submitted by the form
		String username = obtainUsername(request);
		String password = obtainPassword(request);

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

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

        //obtain
		username = username.trim();
		//Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure)
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		//Setting related information
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		//Call authenticate to authenticate
		return this.getAuthenticationManager().authenticate(authRequest);
    }

Two methods need to be called: obtainUsername obtainPassword: (only obtainUsername is listed)

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

obtainUsername---->this.usernameParameter:

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";//Notice the value here

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

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

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

Summarize the points for attention and process:
1. Judge whether it is submitted by post. If so, get the user name and password submitted by the form
2. Get the data, mark it as unauthenticated, set some information in the request into the object, and call authenticate for authentication (it will call the method in userDetailsService written by itself for authentication, that is, check the data)
Specific code:

1.1 judge whether it is a post submission. If so, get the user name and password submitted by the form. At the same time, the details are the page. The name submitted by the form must be written as "username" and "password", otherwise security cannot recognize it. The specific reason is that the source code above has been written

	if (postOnly && !request.getMethod().equals("POST")) {//1. Judge whether it is a post submission
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//If yes, get the user name and password submitted by the form
		String username = obtainUsername(request);
		String password = obtainPassword(request);

2. Get the data, mark it as unauthenticated, set some information in the request into the object, and call authenticate for authentication (it will call the method in userDetailsService written by itself for authentication, that is, check the data)

        //obtain
		username = username.trim();
		//Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure)
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		//Set relevant information
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		//Call authenticate to authenticate
		return this.getAuthenticationManager().authenticate(authRequest);

2.1 there is a method: set the request information into the object, setDetails

protected void setDetails(HttpServletRequest request,
			UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}
	public void setDetails(Object details) {
		this.details = details;
	}

2.2. About the unauthorized problem in the source code:

In the source code:
//Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure)
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

Specific methods of source code:

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
        //Mark as not certified
		setAuthenticated(false);
	}

(Note: the following points are actually detailed explanations for the specific operations in attemptAuthentication)

3. Go deeper and look at the relevant codes of UsernamePasswordAuthenticationToken:

There are two main points: unsuccessful authentication (that is, the construction method in 2.2 above has been described, and the authentication is successful below)
Successful certification:

/**
	 * This constructor should only be used by <code>AuthenticationManager</code> or
	 * <code>AuthenticationProvider</code> implementations that are satisfied with
	 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
	 * authentication token.
	 *
	 * @param principal
	 * @param credentials
	 * @param authorities
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
        //Mark as certified
		super.setAuthenticated(true); // must use super, as we override
	}

4.UsernamePasswordAuthenticationToken inherits from abstractauthenticationtoken, which implements the interface: Authentication view the interface below: Authentication:

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();//Permission set
	Object getCredentials();//Password verification
	Object getDetails();//User details
	Object getPrincipal();//
	boolean isAuthenticated();//Authorized
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;//Set whether to be authorized
}

The above is in-depth in the process of reading the source code. Now go back to the original source code to explore the relevant contents of authorization

5. See the last step in attemptAuthentication, authenticate authentication:

Pass in the unauthenticated information for identity authentication

return this.getAuthenticationManager().authenticate(authRequest);

1.getAuthenticationManager—

	protected AuthenticationManager getAuthenticationManager() {
		return authenticationManager;
	}

Method returns AuthenticationManager
2.AuthenticationManager

public interface AuthenticationManager {}

It is found that it is an interface. Find the implementation class
3. Find his implementation class: ProviderManager. The authentication is implemented here

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {}

Specific operation of authenticate;

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    //Gets the Authentication type passed in Usernamepasswordauthenticationtoken class
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

   		//The previous code obtained the content through the iterator, and now it is a for loop
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {//Determine whether the current AuthenticationProvider is applicable to
                //Usernamepasswordauthenticationtoken Authentication of type class
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
			//The AuthenticationProvider adapted to the current authentication method was successfully found
			try {//Call the authenticate() method of the AuthenticationProvider that is successfully found to adapt to the current authentication method to start authentication
                //If the Authentication is successful, an Authentication object marked as authenticated will be returned
				result = provider.authenticate(authentication);

				if (result != null) {
                    //After successful Authentication, copy the details information in the incoming Authentication object to the authenticated Authentication object
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
            //Authentication failed. Use the parent type AuthenticationManager for authentication
			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;
			}
		}

		if (result != null) {//After successful authentication, take out the relevant sensitive information in the result and require the relevant classes to implement the credentialscontainer interface
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
                //The removal process is to call the eraseCredentials method of the CredentialsContainer interface
				((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
			//The above words have also been confirmed, and relevant information will be released. If you don't, find your father. Anyway, if you want to have it, the following ones will not be repeated
			//Publishing authentication success events
            if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}
    // Parent was null, or didn't authenticate (or throw an exception).
		//Authentication failure: throw a message	
		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);
		}

		throw lastException;

1. Get authentication first, that is, the previous UsernamePasswordAuthenticationToken
2. Get the content through the iterator, and now get the authentication content through the for loop

6. Go back to the beginning: find the method of authentication failure and success (authentication success just now) and find it in its parent class

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//Encapsulate the user object with successful authentication
		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

	/**
	 * Default behaviour for unsuccessful authentication.
	 * <ol>
	 * <li>Clears the {@link SecurityContextHolder}</li>
	 * <li>Stores the exception in the session (if it exists or
	 * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
	 * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
	 * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
	 * </ol>
	 */
	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}

		//Here is to remember that my related operation failed
		rememberMeServices.loginFail(request, response);

		failureHandler.onAuthenticationFailure(request, response, failed);
	}

Let's take a look at the permission access process: it mainly involves two filters: FilterSecurityInterceptor and exceptiontranslation filter

1.ExceptionTranslationFilter catches the thrown exception and handles it

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

        try {
            //Direct release requested by the current end
            chain.doFilter(request, response);
            this.logger.debug("Chain processed normally");
        } catch (IOException var9) {
            throw var9;
        } catch (Exception var10) {
            //Catch and handle the thrown exception. AuthenticationException: no permission
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
            RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (ase == null) {
                ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (ase == null) {
                if (var10 instanceof ServletException) {
                    throw (ServletException)var10;
                }

                if (var10 instanceof RuntimeException) {
                    throw (RuntimeException)var10;
                }

                throw new RuntimeException(var10);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
        }

    }

2.FilterSecurityInterceptor
Judge whether the current request can access the resource according to the resource access permission

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        this.invoke(fi);
    }

invoke: execution method:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }

    }

Core part:

//Judge whether the current request can access the resource according to the resource access permission. Here, you need to go through spring MVC to carry out relevant operations
InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());//Access resource filter
            } finally {
                super.finallyInvocation(token);
            }

3. Sharing authentication information between spring security requests

Look at the source code:

public interface SecurityContext extends Serializable {
	//The essence is to encapsulate Authentication
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 *
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);
}

Look at the construction method in its implementation class, there are encapsulation steps:

public class SecurityContextImpl implements SecurityContext {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private Authentication authentication;

	public SecurityContextImpl() {}

	public SecurityContextImpl(Authentication authentication) {
		this.authentication = authentication;
	}

Let's look at SecurityContextHolder
Many things have been done. The main one is to bind the operation to the current thread:

public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

Second: find a new securityContext object in the current thread and return it if any. If not, create a new securityContext object:

	public static SecurityContext getContext() {
		return strategy.getContext();
	}

Finally, respond through the SecurityContextPersistenceFilter:
This filter is at the front of all filters. It binds Authentication and session. See his dofilter:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (request.getAttribute("__spring_security_scpf_applied") != null) {
            chain.doFilter(request, response);
        } else {
            boolean debug = this.logger.isDebugEnabled();
            request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
            if (this.forceEagerSessionCreation) {
                HttpSession session = request.getSession();
                if (debug && session.isNew()) {
                    this.logger.debug("Eagerly created session: " + session.getId());
                }
            }
			//When the request comes, check whether there is a SecurityContext object in the current session. If there is, it will return. If not, it will create a new SecurityContext and return
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
            SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
            boolean var13 = false;

            try {
                var13 = true;
                //Put the SecurityContext object obtained above into it
                SecurityContextHolder.setContext(contextBeforeChainExecution);
                chain.doFilter(holder.getRequest(), holder.getResponse());
                var13 = false;
            } finally {
                if (var13) {
                    //This is where the response is started and the SecurityContext is fetched for the request
                    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
                    //Remove securityContext
                    SecurityContextHolder.clearContext();
                    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
                    request.removeAttribute("__spring_security_scpf_applied");
                    if (debug) {
                        this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
                    }

                }
            }

            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            SecurityContextHolder.clearContext();
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute("__spring_security_scpf_applied");
            if (debug) {
                this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }

        }
}

Keywords: Java Spring

Added by d-m on Tue, 25 Jan 2022 23:09:19 +0200