基于SpringSecurity OAuth2实现单点登录——应用A是如何重定向到授权服务器的授权地址呢?

1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》

1、前言

  通过前面两篇的内容,我们知道:当第一次(未认证的情况下)访问应用A(http://localhost:8082/index)时,首先,会重定向到应用A的登录http://localhost:8082/login地址(Get请求),然后,又会重定向到授权服务器的http://localhost:8080/oauth/authorize地址上,那么为什么会重定向到授权服务器呢,这中间发生了什么呢?我们继续通过代码进行分析。

2、 OAuth2ClientContextFilter和OAuth2ClientAuthenticationProcessingFilter

  当我们在启动类上添加了@EnableOAuth2Sso注解时,会通过自动配置类OAuth2RestOperationsConfiguration,添加一个过滤器OAuth2ClientContextFilter到Servlet Filter过滤器链中,需要注意的是,这和前面提到的SpringSecurity过滤器链是不一样的,它是直接添加到了Servlet Filter过滤器链中,而且是在FilterChainProxy过滤器链之前的,具体如下:
基于SpringSecurity OAuth2实现单点登录——应用A是如何重定向到授权服务器的授权地址呢?_第1张图片
  同时,也会通过SsoSecurityConfigurer配置类,在SpringSecurity过滤器链中添加一个OAuth2ClientAuthenticationProcessingFilter过滤器,如下所示:
基于SpringSecurity OAuth2实现单点登录——应用A是如何重定向到授权服务器的授权地址呢?_第2张图片
  当重定向到应用A的http://localhost:8082/login登录地址时,经过OAuth2ClientContextFilter过滤器后,再进入到了SpringSecurity过滤器链中的OAuth2ClientAuthenticationProcessingFilter过滤器中。在OAuth2ClientAuthenticationProcessingFilter中,会进行单点登录的认证,即向授权服务器发送登录验证请求,因为没有携带accessToken或code,这个时候就会抛出异常,然后被前面的OAuth2ClientContextFilter过滤器拦截到,然后在OAuth2ClientContextFilter异常处理逻辑中,实现认证授权地址的重定向。

注意:有人可能会好奇,为什么没有被SpringSecurity过滤器链中的异常过滤器拦截呢?其实,这个时候,还没有到SpringSecurity中的过滤器链呢,所以最终会被OAuth2ClientContextFilter捕获。

3、 过滤器的执行流程

  当重定向到应用A的http://localhost:8082/login登录地址时,会进入OAuth2ClientAuthenticationProcessingFilter的doFilter()方法(实际上是在父类AbstractAuthenticationProcessingFilter中定义的),如下所示:

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

	//省略 …… 
	
	Authentication authResult;

	try {
		//在OAuth2ClientAuthenticationProcessingFilter中实现
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}
	catch (InternalAuthenticationServiceException failed) {
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}
	successfulAuthentication(request, response, chain, authResult);
}

  这时候调用attemptAuthentication()方法时,如果验证通过应该执行successfulAuthentication()方法,而如果抛出InternalAuthenticationServiceException 或AuthenticationException 异常时,会执行unsuccessfulAuthentication()方法。但是呢,实际上,在执行attemptAuthentication()方法时,确实抛出了一个异常,不过却不是InternalAuthenticationServiceException 或AuthenticationException 异常,具体抛出了什么异常呢?我们继续Debug代码进行分析,代码如下:

//OAuth2ClientAuthenticationProcessingFilter.java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
		throws AuthenticationException, IOException, ServletException {
		
	OAuth2AccessToken accessToken;
	try {
		accessToken = restTemplate.getAccessToken();
	} catch (OAuth2Exception e) {
		BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
		publish(new OAuth2AuthenticationFailureEvent(bad));
		throw bad;			
	}
	// 省略 ……
}

  在上述attemptAuthentication()方法中,通过调用OAuth2RestTemplate实例的getAccessToken()方法获取授权服务器提供的accessToken,这个时候因为没有accessToken,在内部又会调用acquireAccessToken()方法获取accessToken,代码如下:

//OAuth2RestTemplate.java
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
	OAuth2AccessToken accessToken = context.getAccessToken();

	if (accessToken == null || accessToken.isExpired()) {
		try {
			accessToken = acquireAccessToken(context);
		}
		catch (UserRedirectRequiredException e) {
			context.setAccessToken(null); // No point hanging onto it now
			accessToken = null;
			String stateKey = e.getStateKey();
			if (stateKey != null) {
				Object stateToPreserve = e.getStateToPreserve();
				if (stateToPreserve == null) {
					stateToPreserve = "NONE";
				}
				context.setPreservedState(stateKey, stateToPreserve);
			}
			throw e;
		}
	}
	return accessToken;
}


protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context)
	throws UserRedirectRequiredException {

	AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
		
	// 省略 ……
	
	OAuth2AccessToken accessToken = null;
	accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
	if (accessToken == null || accessToken.getValue() == null) {
		throw new IllegalStateException(
				"Access token provider returned a null access token, which is illegal according to the contract.");
	}
	oauth2Context.setAccessToken(accessToken);
	return accessToken;
}

  在上述的acquireAccessToken()方法中又调用了AuthorizationCodeAccessTokenProvider的obtainAccessToken()方法获取accessToken,继而又调用了obtainAuthorizationCode()方法(因为没有携带code会调用该方法),这个时候,如果accessToken还是为null,就会抛出UserRedirectRequiredException异常(代码略,这个过程会把授权地址相关信息封装到异常信息中)。该异常在OAuth2ClientAuthenticationProcessingFilter中无法被捕获,最终是在OAuth2ClientContextFilter过滤器中被捕获,代码如下:

//OAuth2ClientContextFilter.java
public void doFilter(ServletRequest servletRequest,
	ServletResponse servletResponse, FilterChain chain)
		throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) servletRequest;
	HttpServletResponse response = (HttpServletResponse) servletResponse;
	request.setAttribute(CURRENT_URI, calculateCurrentUri(request));

	try {
		chain.doFilter(servletRequest, servletResponse);
	} catch (IOException ex) {
		throw ex;
	} catch (Exception ex) {
		// Try to extract a SpringSecurityException from the stacktrace
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
				.getFirstThrowableOfType(
						UserRedirectRequiredException.class, causeChain);
		if (redirect != null) {
			redirectUser(redirect, request, response);
		} else {
			if (ex instanceof ServletException) {
				throw (ServletException) ex;
			}
			if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}
			throw new NestedServletException("Unhandled exception", ex);
		}
	}
}

  在上述代码中,我们知道异常最终被捕获Exception 的代码块捕获,其中又执行了redirectUser()方法,该方法就是实现从应用A登录地址重定向到授权服务器授权地址的具体实现了,具体如下:

protected void redirectUser(UserRedirectRequiredException e,
		HttpServletRequest request, HttpServletResponse response)
		throws IOException {

	String redirectUri = e.getRedirectUri();
	UriComponentsBuilder builder = UriComponentsBuilder
			.fromHttpUrl(redirectUri);
	Map<String, String> requestParams = e.getRequestParams();
	for (Map.Entry<String, String> param : requestParams.entrySet()) {
		builder.queryParam(param.getKey(), param.getValue());
	}
	if (e.getStateKey() != null) {
		builder.queryParam("state", e.getStateKey());
	}
	this.redirectStrategy.sendRedirect(request, response, builder.build()
			.encode().toUriString());
}

  在redirectUser()方法中,通过redirectStrategy.sendRedirect()方法,实现了从访问应用A的http://localhost:8082/login登录地址到授权服务器http://localhost:8080/oauth/authorize地址重定向的操作。

4、写在最后

  自此,我们明白了应用A如何实现重定向到授权服务器的授权地址的,那么授权服务器是如何完成授权的呢?我们在后续的内容中继续学习。

你可能感兴趣的:(Spring,Cloud,OAuth2,SpringSecurity,单点登录)