Implementation of single sign on based on spring security oauth2 -- how are unauthenticated requests redirected to the login address (authentication process of spring security)?

1. Foreword

  in the previous article Getting started examples and process analysis In the process of process analysis, when application A is accessed for the first time (without authentication)( http://localhost:8082/index )Will be redirected to the login of APP A http://localhost:8082/login Address (Get request). From the perspective of browser, we can see this situation. What has happened to the server of application A? We analyze through code.

2. Spring security filter chain

  the problem analyzed in this section is actually the logic of spring security about the authentication process. Spring security implements authentication logic through the spring security filter chain. Let's first learn about the core class FilterChainProxy in the spring security filter chain.

2.1,FilterChainProxy


   in spring security, the filter of spring security is not directly embedded into the Servlet Filter, but managed uniformly through FilterChainProxy, that is, the execution of all spring security filters is managed in FilterChainProxy, so we choose to start with FilterChainProxy class for analysis.

   in order to realize the functions described above, the spring security filter is uniformly managed by the FilterChainProxy, and then an internal VirtualFilterChain class is defined internally to represent the filter chain within spring security, in which the doFilter() method is used to execute the filters in the filter chain. As follows:

//FilterChainProxy#VirtualFilterChain, omitting Debug related information
@Override
public void doFilter(ServletRequest request, ServletResponse response)
		throws IOException, ServletException {
	if (currentPosition == size) {
		// Deactivate path stripping as we exit the security filter chain
		this.firewalledRequest.reset();
		//Execute filters in the Web
		originalChain.doFilter(request, response);
	}
	else {//Execute filters in the spring security filter chain
		currentPosition++;
		//All filters in the spring security filter chain are defined in additionalFilters
		Filter nextFilter = additionalFilters.get(currentPosition - 1);
		nextFilter.doFilter(request, response, this);
	}
}

   through the breakpoint, we can view the filter set in the additionalFilters variable, that is, all the filters in the spring security filter chain. The following is the spring security filter in application A, as follows:

3. FilterSecurityInterceptor filter

   by executing the code through Debug, we found that when the FilterSecurityInterceptor filter is executed, the front-end page redirects to the login of application A http://localhost:8082/login Address (Get request). What happens when the FilterSecurityInterceptor filter is executed? We conduct step-by-step analysis through Debug.

   first, we enter the doFilter() method of the FilterSecurityInterceptor filter. In the doFilter() method, we call the invoke() method, and in the invoke() method, we call the beforeInvocation() method of the parent class AbstractSecurityInterceptor, as shown below:

//FilterSecurityInterceptor.java

public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	FilterInvocation fi = new FilterInvocation(request, response, chain);
	invoke(fi);
}
	
public void invoke(FilterInvocation fi) throws IOException, ServletException {
	if ((fi.getRequest() != null)
			&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
			&& observeOncePerRequest) {
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	}else {
		// first time this request being called, so perform security checking
		if (fi.getRequest() != null && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		//To access the address of application A, first obtain the Token in the request through the beforeInvocation() method
		InterceptorStatusToken token = super.beforeInvocation(fi);
		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}
}

In the above code, calling the beforeInvocation() method of the parent class AbstractSecurityInterceptor to get the Token value requested, because the first visit has not been authenticated, so the authentication exception (AccessDeniedException) will be thrown as follows:

//AbstractSecurityInterceptor.java
protected InterceptorStatusToken beforeInvocation(Object object) {
	
	// ... omitted

	Authentication authenticated = authenticateIfRequired();

	// Attempt authorization
	try {
		//It is used to judge whether the current request has permission to access. If there is no permission, an AccessDeniedException exception will be thrown.
		this.accessDecisionManager.decide(authenticated, object, attributes);
	}
	catch (AccessDeniedException accessDeniedException) {
		publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
				accessDeniedException));

		throw accessDeniedException;
	}
	
	// ... omitted

}

  when executing the above code, an AccessDeniedException exception is thrown, and this exception will be caught by the ExceptionTranslationFilter filter, as shown below:

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

		logger.debug("Chain processed normally");
	}catch (IOException ex) {
		throw ex;
	}catch (Exception ex) {
		// Try to extract a SpringSecurityException from the stacktrace
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		RuntimeException ase = (AuthenticationException) throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);
		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 {
			if (ex instanceof ServletException) {
				throw (ServletException) ex;
			}
			else if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}
			throw new RuntimeException(ex);
		}
	}
}

   when an AccessDeniedException occurs, it will be intercepted by the second catch code block in the doFilter() method of the ExceptionTranslationFilter filter, and then handed over to the handlesprinsecurityexception() method for exception handling, as follows:

//ExceptionTranslationFilter.java
private void handleSpringSecurityException(HttpServletRequest request,
	HttpServletResponse response, FilterChain chain, RuntimeException exception)
		throws IOException, ServletException {
	if (exception instanceof AuthenticationException) {
		//Omit debug
		sendStartAuthentication(request, response, chain,
				(AuthenticationException) exception);
	}else if (exception instanceof AccessDeniedException) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
			//Omit debug
			sendStartAuthentication(
					request,
					response,
					chain,
					new InsufficientAuthenticationException(
						messages.getMessage(
							"ExceptionTranslationFilter.insufficientAuthentication",
							"Full authentication is required to access this resource")));
		}else {
			//Omit debug
			accessDeniedHandler.handle(request, response,
					(AccessDeniedException) exception);
		}
	}
}

  in the handlesprinsecurityexception() method, proceed to the next step according to the AuthenticationException or AccessDeniedException exception type. Because we threw the AccessDeniedException exception in the previous step, we will execute the sendStartAuthentication() method (in fact, both types of exceptions execute this method, but the parameters are different). The sendStartAuthentication() method is implemented as follows:

//ExceptionTranslationFilter
protected void sendStartAuthentication(HttpServletRequest request,
	HttpServletResponse response, FilterChain chain,
		AuthenticationException reason) throws ServletException, IOException {
	SecurityContextHolder.getContext().setAuthentication(null);
	requestCache.saveRequest(request, response);
	logger.debug("Calling Authentication entry point.");
	authenticationEntryPoint.commence(request, response, reason);
}

  in the sendStartAuthentication() method, the comment() method of authenticationEntryPoint is called again. The authenticationEntryPoint here defaults to the LoginUrlAuthenticationEntryPoint instance. The final page Jump is also in the comment() method, in which the sendRedirect() method of redirectStrategy is called to complete the final redirection. The comment() method of LoginUrlAuthenticationEntryPoint is defined as follows:

//LoginUrlAuthenticationEntryPoint.java
public void commence(HttpServletRequest request, HttpServletResponse response,
	AuthenticationException authException) throws IOException, ServletException {
	
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			if (logger.isDebugEnabled()) {
				logger.debug("Server side forward to: " + loginForm);
			}
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}else {
		//Build redirect address
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	//The redirectUrl here corresponds to http://localhost:8082/login address
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

  so far, execute redirectstrategy The sendredirect () method can redirect to the login address of application A.

4. Write at the end

   in this section, we mainly analyze how unauthenticated requests are redirected to the login address (current application). In the next section, we start to analyze how the authorization server authorizes. Please look forward to it!!!

Keywords: Spring Cloud Spring Security oauth2

Added by Helios on Sun, 30 Jan 2022 10:44:50 +0200