SSO单点登录原理深度解剖(二)

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方法,

然后就重定向到

http://server:10880/cfgs/oauth2/authorize?response_type=code&client_id=cth-messaging-client&scope=openid&state=QNjecYH3t_igg4JuvANJt0Po8nBeFAB33PDQT0jMttk%3D&redirect_uri=http://127.0.0.1:11980/cth/api/getCode&nonce=BRHiKYNikI4XBCaa42tNrOSn8RgQQMorqr91FfgRD20

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()方法,就实现了重定向到授权服务器的登录地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值