1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》
通过前面两篇的内容,我们知道:当第一次(未认证的情况下)访问应用A(http://localhost:8082/index)时,首先,会重定向到应用A的登录http://localhost:8082/login地址(Get请求),然后,又会重定向到授权服务器的http://localhost:8080/oauth/authorize地址上,那么为什么会重定向到授权服务器呢,这中间发生了什么呢?我们继续通过代码进行分析。
当我们在启动类上添加了@EnableOAuth2Sso注解时,会通过自动配置类OAuth2RestOperationsConfiguration,添加一个过滤器OAuth2ClientContextFilter到Servlet Filter过滤器链中,需要注意的是,这和前面提到的SpringSecurity过滤器链是不一样的,它是直接添加到了Servlet Filter过滤器链中,而且是在FilterChainProxy过滤器链之前的,具体如下:
同时,也会通过SsoSecurityConfigurer配置类,在SpringSecurity过滤器链中添加一个OAuth2ClientAuthenticationProcessingFilter过滤器,如下所示:
当重定向到应用A的http://localhost:8082/login登录地址时,经过OAuth2ClientContextFilter过滤器后,再进入到了SpringSecurity过滤器链中的OAuth2ClientAuthenticationProcessingFilter过滤器中。在OAuth2ClientAuthenticationProcessingFilter中,会进行单点登录的认证,即向授权服务器发送登录验证请求,因为没有携带accessToken或code,这个时候就会抛出异常,然后被前面的OAuth2ClientContextFilter过滤器拦截到,然后在OAuth2ClientContextFilter异常处理逻辑中,实现认证授权地址的重定向。
注意:有人可能会好奇,为什么没有被SpringSecurity过滤器链中的异常过滤器拦截呢?其实,这个时候,还没有到SpringSecurity中的过滤器链呢,所以最终会被OAuth2ClientContextFilter捕获。
当重定向到应用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地址重定向的操作。
自此,我们明白了应用A如何实现重定向到授权服务器的授权地址的,那么授权服务器是如何完成授权的呢?我们在后续的内容中继续学习。