前後端分離或AJAX下的CAS-SSO跨域流程分析

前後端分離或AJAX下的CAS-SSO跨域流程分析

公司要做用戶中心,不同的產品模塊要通過用戶中心接通單點登錄和單點退出。項目各種各樣,有的是傳統的單體應用,有的是現在很多前後分離REST的微服務。傳統的單體服務通過CAS簡單的部署很容易就能接通,但是前後分離的REST方式,需要對cas-client進行一些改造,同時,對接流程也需要根據約定發生相應的改變。下面,把我關於此問題的思考與項目的實現分享如下。

1 名詞說明

  • cas-server 單點登錄服務,負責管理TGT,給cas-service頒發ST
  • cas-service 需要接入cas-server(單點登錄服務)的應用服務。
  • cas-client 單點登錄客戶端,cas-service接入cas-server需要依賴cas-client

2 問題引入

CAS-SSO簡介請參考文章CAS單點登錄(一)——初識SSO,使用官方流程圖描述了cas-service接入cas-server的一般流程。通過瀏覽器發送HTTP請求,服務端發送302跳轉指令進行登錄驗證。這種方式適合傳統的單體項目,前後不分離,前端使用JSP或者其他模板引擎的方式。在這種情況下,前後不分離,不用做特殊處理,瀏覽器就會把Cookie中的登錄狀態信息等帶入到cas-service中。但是目前我們的開發方式中,開發階段使用dev-server,生產階段是打包成靜態文件放入單獨的靜態資源服務器中,如Nginx。

前端對後端的調用方式,通常也是採用AJAX(XMLHttpRequest)請求:這是瀏覽器內部的XMLHttpRequest對象發起的請求,瀏覽器會禁止其發起跨域的請求,主要是爲了防止跨站腳本僞造的攻擊(CSRF)。不但如此,上面提到過傳統的HTTP請求,cas-service中的cas filter 過濾到沒有登錄的會給瀏覽器發送一個302重定向到cas-server進行登錄,登錄成功後cas-server會再給瀏覽器發送一個302重定向到cas-service中繼續完成業務操作。 但是前後分離的AJAX的調用方式,是不支持302重定向的。

對於前後不分離,也有一些操作,是AJAX調用,當登錄過期的時候,再發送AJAX請求,cas-service發送的重定向指令,AJAX是識別不了的。

3 問題的解決思路

基於以上問題,分別給出了一下幾點解決思路。

3.1 跨域問題

基於跨域問題,以SpringBoot爲例,後端開啓可以跨域,允許跨域攜帶認證方式請求。AJAX在發送跨域請求時也做相同配置,跨域的時候既可以把Cookie中存的cas-service中的session回話信息傳入後端,維持登錄到會話。例如:js 的AJAX可以這麼寫,對接的時候應該去掉下面的 timeout: 60000,

$.ajax({
      type: type,
      url: url,
      data: newParam,
      dataType: 'json',
      xhrFields: {
        withCredentials: true,
      },
      crossDomain: true,
      headers: {
        tempToken: tempToken,
        token:token,
      },
      timeout: 60000,
      success: function(data) {
        deferred.resolve(data);
      },
})

上面的代碼中,關於跨域,主要的配置是:

      xhrFields: {
        withCredentials: true
      },
      crossDomain: true

以Spring Boot爲例的後端的跨域配置如下:

@Configuration
public class GlobalCorsConfig {
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:8000");
        config.addAllowedHeader("Cookie");
        config.addAllowedHeader("tempToken");
        config.addAllowedHeader("token");
        config.addAllowedMethod(RequestMethod.GET.name());
        config.addAllowedMethod(RequestMethod.POST.name());
        config.addAllowedMethod(RequestMethod.PUT.name());
        config.addAllowedMethod(RequestMethod.DELETE.name());

        // CORS 配置對所有接口都有效
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> filter = new FilterRegistrationBean<>(new CorsFilter(source));
        // 跨域的過濾器應該比較靠前
        filter.setOrder(Integer.MIN_VALUE + 1);
        return filter;
    }
}

以上代碼便描述了跨域的處理,cas-service中的跨域配置中,允許的請求源頭,不要設置爲星號 *

3.2 AJAX無法識別302問題

關於AJAX無法識別302問題,cas-service可以自定義個過濾器替換cas-service的默認鑑權的過濾器,當發現客戶端請求需要的登錄的時候,不發送302跳轉。發送一個json串,瀏覽器通過獲取的數據,javascript主動通過window.localtion.href=https://sso.example.com跳轉到cas的登錄頁面,cas-service登錄成功後跳轉到cas-client的後端地址去進行登錄處理的方法中,在這個方法中主要就是產生Session,和前端需要與後端鑑權的token。由於是瀏覽器主動發起的對後端的Http get請求,cas-service後端可以把session信息寫入到後端的域名的瀏覽器Cookie中,可以在下一次調用的時候進行會話保持。
同時在這個後端處理方法中,發送一個302請求給瀏覽器,這個302重定向攜帶token或者臨時token放到url參數中,跳轉到前端的進行登錄處理的路由中,在前端處理中把token信息或者臨時的token信息存入到前端域名的Cookie中,再次發送AJAX請求的時候token信息作爲Header信息,從後端的域名下獲取Cookie信息進行請求。

關於cas-service 中的過濾器,已經在uc-cas-client中進行封裝,直接引入依賴即可,至於返回的json數據,目前是以下數據

{
	"success": false,
	"code": -1,
	"message": "請登錄",
	"data": {
		"targetUrl": "https://sso.wuss.com:8443/cas/login?service=http%3A%2F%2Fapp1.wuss.com%3A10080%2Fdemo-singleton%2Faccount%2FloginProcess"
	}
}

如上json串中,通過一個code判斷需要登錄,targetUrl作爲需要重定向的url,可以看到,有一個service的url參數,這個參數就是你的cas-service的後端地址host+contextPath+後端處理方法的路徑。前端的js接受的這個串,可以在AJAX的回調函數中做以下類似操作

 if (response.code === -1) {
      let originUrl = encodeURI(window.location.href) ;
      let redirectUrl = response.data.targetUrl.concat('?originUrl=').concat(originUrl);
      console.log(redirectUrl);
      window.location.href = redirectUrl;
    }

上述代碼中,可以看到變量 originUrl 記錄了當前的請求的路由,並且作爲url參數傳入到了重定向的地址。那cas-server登錄成功後,重定向後端處理方法的時候就會把這個originUrl給帶上。後端重定向到前端登錄處理路由的時候,不但要攜帶token或者臨時token作爲url參數,還有originUrl,前端處理完登錄後重新重定向到originUrl,這樣就回到了登錄之前操作的界面。

另外,在後端的登錄後處理方法中,你可以通過以下代碼獲取登錄後的用戶信息。

AttributePrincipal principal = AssertionHolder.getAssertion().getPrincipal();
//獲取username
String username = principal.getName();
//獲取cas-server登錄成功後返回的用戶屬性
principal.getAttributes();

代碼獲取到線程變量中的已經登錄到username和一些設置好的登錄後返回的用戶屬性。

下面我以React爲例,給出前端的登錄處理路由代碼:

import React, { PureComponent } from 'react';
import { setCookie } from '@/utils/utils';

class LoginProcess extends PureComponent {
    
  componentWillMount() {
    const { location } = this.props;
    const { query } = location;
    const d = new Date();
    d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000));
    const expires = 'expires='.concat(d.toGMTString());

    setCookie('tempToken', query.tempToken);
    setCookie('test', 'wuss');

    window.location.href = query.originUrl;
  }
  
  render() {
    return (
      <div>
        登錄後處理
      </div>
    );
  }
}

export default LoginProcess;

react 通過以上代碼,在this.props.location中獲取瀏覽器url參數,並且把臨時token,或者token存入Cookie,然後獲取originUrl並且重定向到。等AJAX再次向後端發送請求,根據以上配置,就會攜帶token放到Header中作爲用戶鑑權,攜帶後端域名下的Cookie中的Session信息,保持登錄會話。

如果你第一次使用的是一個臨時token,那麼,需要做一次通過臨時token換取真實token的操作。

4 總體流程

基於上面的問題描述與解決思路,cas-service與cas-server對接的總體流程如下圖所示。下圖中app1-backend 就是cas-service1的後端。

前後分離的cas-service和cas-server的對接

通過上圖可知,如果同一個前端的域名對接了多個後端的域名,多個後端中至少有兩個需要登錄的話,cas-service1登錄後會給前端返回token,cas-service2登錄成功之後也會給前端返回token,兩個同名的話,前端就無法區分到底是哪一個。在這種情況下,建議後端返回的token加上一個自己service的前綴,方便前端去維護。

上面提到的tempToken ,可以理解爲一個校驗碼的概念,如果你覺得麻煩,可以直接返回前端token。加上tempToken只是避免了token在302跳轉的一瞬間出現在瀏覽器的地址欄上。

5 ST與Session的存儲與單點退出

上圖以及上文,當登錄成功cas-server給瀏覽器發送302指令跳轉到cas-service的後端的時候,會攜帶一個名字爲ticket的URL參數,這個ticket就是由TGT生成的ST,事實上,當cas-service的filter發現有一個叫ticket的URL參數的時候,就會拿這個ST去cas-server做校驗,校驗通過後會生成一個保持登錄狀態的Session,這個Session中封裝了成功登錄後返回的一些用戶信息,當前配置的ST只能去校驗一次,再次去cas-server中去校驗的話將不會被通過。那麼,這個ST就與Session做了唯一關聯,事實上,當發起單點退出的時候,cas-server會給cas-service發送一個logout的HTTP POST請求,這個請求會被cas-service filter攔截,使用這個這個請求中攜帶的ST清除已經登錄產生的Session,便達到了單點退出的目的。

那麼ST與Session這個關係時如何存儲的呢?這裏,分兩種情況:

  • cas-service單機部署:單機部署的時候,是通過一個Map<String, HttpSession>對象存儲,這個map的key就是ST。
  • cas-service集羣部署:當你採用集羣部署的時候,目前代碼實現是通過Redis做了一個Session共享的服務器,在Redis存儲了ST與SessionID的關係。當logout請求發出來,就是從ST中獲取SessionID,然後通過SessionID清除Session。

以上,之所以做ST與Session關係的存儲,主要是爲了應對單點退出。當cas-server接到單點退出的請求的時候,它會嘗試向SSO會話期間請求對CAS進行身份驗證的每個應用程序發送註銷消息。

關於單點退出,請查看demo中的AccountController類,url爲logout的方法

6 關於過期

  • TGT過期,cas-server會向每個已經登錄的cas-service發送一個logout請求,並且清除已經登錄的Session,見上文。目前cas-server設置的TGT過期爲最近兩個小時沒有使用則過期。如果TGT過期,則跳轉重新登錄即可

  • Session過期,跳轉重新登錄。

  • 建議取消JWTFilter對token過期的驗證,以Session過期爲準。另外,請保證cas-service的filter執行優先於你的JWTFilter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章