天天看點

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

作者:微衆開源WebankOS
社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

社群開發者:LeoJie

GitHub :CCweixiao

座右銘:心之所向,素履以往

  • 1. 背景
  • 2. SSO 登入流程概述
  • 3. SSO 接入流程細節
    • 3.1 實作接口 SSOInterceptor
    • 3.2 前端代碼的一些配合修改
    • 3.3 SSO 登出
    • 3.4 SSO 登入後 Cookie 丢失問題
  • 4. 感覺可以繼續優化的地方

Linkis 的管理台和 DSS 目前支援以下使用者登入體系:

  • 接入 LDAP 登入
  • 接入 SSO 單點登入
  • token 登入方式
  • 代理使用者模式

除 SSO 單點登入之外,其他三種登入的接入方式都很容易實作,可以參考 DSS 的 wiki 文檔:

https://github.com/WeBankFinTech/DataSphereStudio/wiki/Login

SSO 登入的接入方式雖然也有文檔描述,但還不夠細節,同時我使用的 linkis 和 dss 的版本分别為1.0.3和dev-1.0.1,SSO 相關功能的代碼還存在些許瑕疵,不仔細測試的話,估計會遇到一些問題,而且,對于剛剛接觸的夥伴,這塊的代碼其實不好在本地 DEBUG,代碼也較為複雜,需要前後端配合進行測試。

鑒于此,為大家分享一下我的實作過程,以供參考。

SSO 的概念以及 SSO 接入的必要性在此不再贅述,網上有很多資料可以參考。雖然每家公司 SSO 的接入方式,在實作細節上或有所差異,但原理或思路應該大緻相同。以我們公司的 SSO 登入流程為例:

  1. 使用者通路 DSS&Linkis 中受保護的 api,Gateway 中的 SecurityFilter 會從 Cookie 緩存中比對使用者名,第一次登入或 Cookie 失效被清理時,SecurityFilter 擷取到空的使用者名後就會觸發 SSO 的登入邏輯,首先,背景标記此次請求的響應狀态為 401,同時,背景會把 SSO 的配置資訊(enableSSO、SSOURL 以及 SSOLogoutURL)傳回給前端。SSOLogoutURL 是我增加的一個傳回值,前端拿到 SSO 的登出 URL 之後,跳轉到該位址,就可以實作使用者登入登出的操作。
  2. 前端響應攔截器捕獲到 401 的響應狀态,同時擷取到 SSO 的配置資訊後,浏覽器會跳轉到 SSO 掃碼登入頁。
  3. 使用者在掃碼頁成功掃碼或登入後,SSO 伺服器會産生一條 token 記錄,該 token 字元串會作為一個 query param 參數拼接到回跳位址(DSS&Linkis 首頁)的後面,然後回跳。
  4. DSS&Linkis 的前端 router 解析路由位址上攜帶的 token 參數,把解析到的 token 字元串存儲到浏覽器的緩存中,後續,全局 request 從緩存中拿到 token,放到 request 的 header 中(或 cookie),再次請求 DSS&Linkis 的背景
  5. DSS&Linkis 背景解析 request header 中的 token 參數,請求 SSO 服務,校驗 token 的有效性,去拿使用者的資訊,成功擷取到使用者資訊後,生成 Cookie,存儲在背景緩存并傳給前端,前端緩存該 Cookie,便于下次請求時攜帶(浏覽器關閉後該 Cookie 就失效啦,由 cookie 的 max-age 屬性決定)

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

InternalSSOInterceptor

重點實作的幾個方法,每家公司的 SSO 邏輯都不一樣,是以,此處隻說明每個接口方法的作用。

/**
    * 如果打開SSO單點登入功能,目前端跳轉SSO登入頁面登入成功後,前端再次轉發請求給gateway。
    * 使用者需實作該接口,通過Request傳回user
    * 解析token,請求SSO服務,校驗token有效性,擷取使用者資訊
    * @param gatewayContext
    * @return
    */
  def getUser(gatewayContext: GatewayContext): String

  /**
    * 通過前端的requestUrl,使用者傳回一個可跳轉的SSO登入頁面URL。
    * 要求:需帶上原請求URL,以便登入成功後能跳轉回來
    * 生成回跳位址,傳回給前端,跳轉操作是由前端完成的
    * @param gatewayContext
    * @return
    */
  def redirectTo(gatewayContext: GatewayContext): String

  /**
    * gateway退出時,會調用此接口,以保證gateway清除cookie後,SSO單點登入也會把登入資訊清除掉
    * 清除cookie資訊,通路SSO服務的登出接口,使原有token失效,然後前端重新跳轉到SSO掃碼頁面
    * @param gatewayContext
    */
  def logout(gatewayContext: GatewayContext): Unit

  /**
   * 内部SSO 實作時需要一個登出的跳轉位址,前端拿到後跳轉到該位址上就可以實作SSO token的登出,(此方法是根據自己的情況做的擴充)
   * @param gatewayContext
   * @return
   */
  def logoutRedirectTo(gatewayContext: GatewayContext): String
           

以下對前端代碼的一些調整,僅針對此版本 SSO 內建過程中遇到的一些問題的解決,後續版本可能對該功能有所優化,包含代碼層面。

api.js

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

api-sso

API 調用時需要在此處捕獲下 401,然後擷取 SSO 的配置資訊後放入浏覽器緩存中。

router.js

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

router-js

路由跳轉時,解析路由位址中的 param 參數——token,然後把 token 字元串放到浏覽器緩存中。此處還用到util.js中的一個工具方法:

/**
   * 擷取路由位址中的參數
   * @param name
   * @returns {string|null}
   */
  getUrlParam(name) {
    const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
    const r = window.location.search.substr(1).match(reg)
    if (r != null) return unescape(r[2])
    return null // 傳回參數值
  },
           

token 緩存之後,再回到api.js的全局 request 處,要為每一個 request 增加一個名為'x-token'的 header。

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

api-header

這樣每次請求都會攜帶 token 傳給後端,(此處也可以考慮用 cookie 存儲,避免 token 字元串直接參與網絡傳輸)

登出的邏輯就是清除浏覽器緩存的 token,跳轉 SSO 的登出 URL,然後同時,後端會執行登出邏輯,并清除後端辨別使用者登入狀态的 Cookie 記錄。

其實,Linkis 0.11 版本修複了這個問題。詳見https://github.com/apache/incubator-linkis/issues/489

在 linkis-1.0.3 中這個 BUG 又出來了,可能是代碼的合并問題,此異常具體的表現是:

SSO 登入成功,但接口依舊報未登入異常,

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

401

原因是,浏覽器 Cookie 緩存中沒有使用者的登入資訊,導緻,後端會持續進行 SSO 的登入操作。

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

cookie

修改代碼後問題解決:

社群開發者專欄 | LeoJie:Linkis實踐--內建SSO登入

resolve

後端使用者登入成功後,登入狀态的 Cookie 存儲時設定的 maxAge 為-1,即浏覽器關閉後 Cookie 就被清除,其實可以設定 Cookie 的存活時間久一點,因為 SSO 的 token 的存活周期本身就比較長。

前端擷取到 token 後,存儲在 Cookie 中,而不是放在請求頭中與後端進行互動。

前端代碼

storage.set('x-token', token, 'cookie', 7 * 24 * 3600)

// 全局Request那裡就不需要把token放到每一個request請求的頭部啦
           

後端 token 擷取

public String getUser(GatewayContext gatewayContext) {
        List<Cookie> cookieList = gatewayContext.getRequest().getCookies().entrySet().stream()
                .flatMap(x-> Arrays.stream(x.getValue()))
                .filter(c-> "x-token".equals(c.getName()))
                .collect(Collectors.toList());
        Cookie cookie = null;
        if (!cookieList.isEmpty()) {
            cookie = cookieList.get(0);
            LOG.info("SSO_COOKIE_NAME {}", cookie.getName());
            LOG.info("SSO_COOKIE_VALUE {}", cookie.getValue());
            LOG.info("SSO_COOKIE_MAX_AGE {}", cookie.getMaxAge());
            LOG.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        }
        if (cookie == null) {
            return "";
        }
        String token = cookie.getValue();
        if (StringUtils.isBlank(token)) {
            return "";
        }
        ... ...
    }
           

SSO 和 LDAP 雙登入,有些特殊賬戶,如管理者賬号和部門共享賬号,不能掃碼進行登入,也不好在 SSO 賬戶體系中加時,可以配合 LDAP 實作雙登入。