天天看點

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?

1、《入門示例和流程分析》

2、《未認證的請求是如何重定向到登入位址的》

3、《應用A是如何重定向到授權伺服器的授權位址呢?》

4、《授權伺服器是如何實作授權的呢?》

5、《登入通路應用A後再通路應用B會發生什麼呢?》

1、前言

  前面我們已經完整的把應用A從通路、授權登入、擷取code、換取accessToken和實作應用A的正常通路的整個過程分析了一遍,那麼如果已經成功通路了應用A後,再直接跳轉通路應用B會發生什麼呢?讓我們一起從代碼中找到答案吧!

2、浏覽器視角

2.1、通路應用B

  當已經成功通路應用A後,再通路應用B(http://localhost:8083/index)時,會重定向到http://localhost:8083/login位址,如下所示:

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?
2.2、重定向到授權伺服器

  當通路前面重定向到的http://localhost:8083/login位址時,又會被重定向到授權伺服器的授權位址,如下所示:

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?
2.3、重新 重定向到應用B的登入

  當通路授權伺服器授權接口後,又重定向到了應用B的登入界面,不過這個時候,帶了code參數。

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?
2.4、重新 重定向到應用B的通路頁面

  當重定向到帶code參數的應用B的登入位址後,會再重定向到應用B的通路頁面。這個過程,使用者不需要再輸入使用者名和密碼進行登入了。

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?

  至此,我們從浏覽器視角,分析了請求應用B時,請求連結的重定向過程。下面我們開始從代碼層面分析,為什麼會這樣重定向。

3、重定向到應用B的登入位址

  這一步的實作,和《未認證的請求是如何重定向到登入位址的》中是完全一樣的。首先,我們進入FilterSecurityInterceptor過濾器的doFilter()方法,在doFilter()方法中又調用了invoke()方法,而在invoke()方法中,又調用了父類AbstractSecurityInterceptor的beforeInvocation()方法,來擷取請求需要的Token值,因為第一次通路,還沒有進行認證,是以會抛出認證異常(AccessDeniedException ),抛出了AccessDeniedException 異常,這個異常就會被ExceptionTranslationFilter過濾器捕獲,然後,經過異常處理,最終會跳轉到應用B的登入界面。 前面的博文中已經詳細分析了,這裡不再重複。

4、重定向到授權伺服器的授權位址

  這一步的實作,和《應用A是如何重定向到授權伺服器的授權位址呢?》中的邏輯是一樣的。首先,在OAuth2ClientAuthenticationProcessingFilter中,會進行單點登入的認證,即向授權伺服器發送登入驗證請求,因為沒有攜帶accessToken或code,這個時候就會抛出異常,然後被前面的OAuth2ClientContextFilter過濾器攔截到,然後在OAuth2ClientContextFilter異常處理邏輯中,實作認證授權位址的重定向。

5、授權伺服器如何進行授權

  在這一步中,和《授權伺服器是如何實作授權的呢?》類似,不過因為這次我們沒有再跳轉到統一登入界面,而是直接通過重定向完成了授權,為什麼不需要重新登入了呢?我們下面跟着代碼分析一下。

  在《授權伺服器是如何實作授權的呢?》中,之是以會跳轉到登入界面是因為應用A進行授權請求(/oauth/authorize)時,其中的principal參數為空,即沒有認證資訊,這個時候就會抛出InsufficientAuthenticationException異常,然後被SpringSecurity過濾器鍊中的異常過濾器攔截,并重定向到授權伺服器的登入位址。而這裡之是以沒有重定向到登入界面,其實就是因為這次的認證授權請求,攜帶了principal資訊,我們下面分析應用B發送認證授權請求,是如何攜帶了使用者資訊的。

  首先,當重定向到授權伺服器授權位址的時候,浏覽器會帶有對應的緩存資訊,如下所示:

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?

  那麼,在授權伺服器,是不是根據這些緩存資訊,我們可以查詢對應的使用者資訊呢,答案是肯定的,該過程就是在授權伺服器的SecurityContextPersistenceFilter過濾器中完成的。我們下面開始分析SecurityContextPersistenceFilter是如何擷取使用者資訊的。

  在請求到達授權請求(/oauth/authorize)方法之前,會先經過SpringSecurity的過濾器,其中SecurityContextPersistenceFilter過濾器就是用來初始化上下文資訊的。

  我們先來分析其doFilter()方法,代碼如下:

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

	// 省略 ……

	HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
			response);
	// 根據請求,擷取上下文資訊,這裡是邏輯的核心。
	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

	try {
		SecurityContextHolder.setContext(contextBeforeChainExecution);

		chain.doFilter(holder.getRequest(), holder.getResponse());

	}
	finally {
		// 省略 ……
	}
}
           

  正如代碼中注釋,repo.loadContext()方法是擷取請求上下文的核心,我們繼續分析該方法,該方法在HttpSessionSecurityContextRepository類中定義,具體實作如下:

public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
	HttpServletRequest request = requestResponseHolder.getRequest();
	HttpServletResponse response = requestResponseHolder.getResponse();
	HttpSession httpSession = request.getSession(false);

	SecurityContext context = readSecurityContextFromSession(httpSession);

	if (context == null) {
		//省略 debug ……
		context = generateNewContext();
	}
	SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
			response, request, httpSession != null, context);
	requestResponseHolder.setResponse(wrappedResponse);

	requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
			request, wrappedResponse));

	return context;
}
           

  其中,又通過readSecurityContextFromSession()方法讀取SecurityContext 資訊,實作如下:

private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
	final boolean debug = logger.isDebugEnabled();
	if (httpSession == null) {
		if (debug) {
			logger.debug("No HttpSession currently exists");
		}
		return null;
	}
	Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
	if (contextFromSession == null) {
		//省略 debug ……
		return null;
	}
	// We now have the security context object from the session.
	if (!(contextFromSession instanceof SecurityContext)) {
		//省略 debug ……
		return null;
	}
	//省略 debug ……
	
	// Everything OK. The only non-null return from this method.
	return (SecurityContext) contextFromSession;
}
           

  在readSecurityContextFromSession()方法中,首先嘗試從httpSession中擷取對應上下文,對應的key=SPRING_SECURITY_CONTEXT,然後判斷該上下文是否是SecurityContext類型,不是的話也直接傳回null,最後如果都符合條件,就強轉成SecurityContext類型并傳回。傳回值,包括了principal參數值,是以在授權請求(/oauth/authorize)方法中,我們就可以直接從上下文中擷取,而不會在跳轉到登入界面讓使用者進行登入。

基于SpringSecurity OAuth2實作單點登入——登入通路應用A後再通路應用B會發生什麼呢?

6、寫在最後

  這篇博文中,我們分析了登入通路應用A後再通路應用B請求經過的授權過程。至此,我們基于SpringSecurity OAuth2實作單點登入的分析就全部完成了,當然在其中還涉及到了一些問題沒有深入分析(比如該博文中擷取的key=SPRING_SECURITY_CONTEXT的上下文是何時存儲到Session的等),後續在使用SpringSecurity的過程中,我們再繼續學習和分享。

繼續閱讀