1、前言
我们知道:第一次(未认证的情况下)访问client A(http://localhost:11980/cth/time)时,会重定向到client A的http://localhost:11980/cth/oauth2/authorization/messaging-client-oidc地址(Get请求),然后,又会重定向到授权服务器的http://server:10880/cfgs/oauth2/authorize?response_type=code&client_id=cth-messaging-client&scope=openid&state=3XIsMHi6vQPGm5MxqtHmqH5D90nKuBN_Af4jMJAoVVo%3D&redirect_uri=http://127.0.0.1:11980/cth/api/getCode&nonce=V4cRB0El04KuXdTfh61LWNZa1cSUn1xKWIRdX5DrdZg地址上,那么为什么会重定向到授权服务器呢,这中间发生了什么呢?我们继续通过代码进行分析。
2、FilterChainProxy
这里我们还是要回到FilterChainProxy。我们可以看到我们再一次进入客户端的FilterChainProxy。如下图所示,我们依然得到了这些过滤器
但是与重定向到登录页面不同的是,我们进入到了OAuth2AuthorizationRequestRedirectFilter 。
判断是否是授权登录请求,进入resolve方法:
进入resolveRegistrationId:
pattern是/oauth2/authorization/{registrationId},因此可解析出registrationId是messaging-client-oidc。
继续执行getAction方法:
请求中没有action参数,返回默认的:login
debug下一步,进入resolve:
denbug下一步,发起重定向:
跟着debug,我们看到了sendRedirectForAuthorization的源码:
首先执行saveAuthorizationRequest方法,即保存本次请求相关的信息,以用于三方平台回调时可以再次获取,例如当回调时需要检查state参数是否一致,以保证安全;
进入到sendRedirect方法,
然后就重定向到
debug放行,后续Filter不被执行,页面渲染授权登录页面。
2、重定向到授权登录页
当重定向到授权服务器http://localhost:10880/oauth2/authorize地址时,这时候就需要从授权服务器的角度来进行分析了。首先,当我们访问http://localhost:10880/oauth/authorize地址时,这个时候,也会经过一系列的过滤器,随着请求跳转到授权服务器,我们还是debug到FilterChainProxy:
我们可以看到有23个Filters.最终被ExceptionTranslationFilter捕获到AccessDeniedException异常。ExceptionTranslationFilter#doFilter 这个方法的主要作用就是对授权异常进行处理,捕获到了AccessDeniedException异常,当出现AccessDeniedException 异常时,会被ExceptionTranslationFilter过滤器的doFilter()方法中第二个catch 代码块进行拦截,然后交由handleSpringSecurityException()方法进行异常的处理,具体如下:
如下是handleSpringSecurityException方法的代码:
//ExceptionTranslationFilter.java(org.springframework.security.web.access)
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
在handleSpringSecurityException()方法中,根据AuthenticationException或AccessDeniedException异常类型,进行下一步执行,因为我们上一步抛出的是AccessDeniedException异常,所以会执行其中handleAccessDeniedException()的方法(其实两类异常都是执行这个方法,只不过参数不一样而已)。handleAccessDeniedException()方法的实现如下:
//ExceptionTranslationFilter.java(org.springframework.security.web.access)
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
从debug到的情况看,isAnonymous 是true.
sendStartAuthentication的代码如下:
//ExceptionTranslationFilter.java(org.springframework.security.web.access)
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
在sendStartAuthentication()方法中, 又调用了authenticationEntryPoint的commence()方法,这里的authenticationEntryPoint是DelegatingAuthenticationEntryPoint实例,最终的页面跳转也是在commence()方法中。
下面是DelegatingAuthenticationEntryPoint的commence方法:
//DelegatingAuthenticationEntryPoint.java(org.springframework.security.web.authentication)
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
if (requestMatcher.matches(request)) {
AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
logger.debug(LogMessage.format("Match found! Executing %s", entryPoint));
entryPoint.commence(request, response, authException);
return;
}
}
logger.debug(LogMessage.format("No match found. Using default entry point %s", this.defaultEntryPoint));
// No EntryPoint matched, use defaultEntryPoint
this.defaultEntryPoint.commence(request, response, authException);
}
从debug中我们可以看到,entryPoint是LoginUrlAuthenticationEntryPoint,于是进入LoginUrlAuthenticationEntryPoint的commce方法。
LoginUrlAuthenticationEntryPoint的commence方法如下所示:
//LoginUrlAuthenticationEntryPoint.java(org.springframework.security.web.authentication)
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (!this.useForward) {
// redirect to login page. Use https if forceHttps true
String redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return;
}
String redirectUrl = null;
if (this.forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS. When that request is received,
// the forward to the login page will be used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl != null) {
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return;
}
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
logger.debug(LogMessage.format("Server side forward to: %s", loginForm));
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
我们从debug的情况看,在这里拼出了要跳转的url。所以没经过认证的地址会跳到这个地址,最后通过调用redirectStrategy的sendRedirect()方法来完成最终的重定向。
下面是DefaultRedirectStrategy的sendRedirect方法。
//DefaultRedirectStrategy(org.springframework.security.web)
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Redirecting to %s", redirectUrl));
}
response.sendRedirect(redirectUrl);
}
至此,通过执行redirectStrategy.sendRedirect()方法,就实现了重定向到授权服务器的登录地址。