10張流程圖+部署圖,講透單點登錄原理與簡單實現!

一、單系統登錄機制

1、http無狀態協議

web應用採用browser/server架構,http作爲通信協議。http是無狀態協議,瀏覽器的每一次請求,服務器會獨立處理,不與之前或之後的請求產生關聯,這個過程用下圖說明,三次請求/響應對之間沒有任何聯繫

但這也同時意味着,任何用戶都能通過瀏覽器訪問服務器資源,如果想保護服務器的某些資源,必須限制瀏覽器請求;要限制瀏覽器請求,必須鑑別瀏覽器請求,響應合法請求,忽略非法請求;要鑑別瀏覽器請求,必須清楚瀏覽器請求狀態。既然http協議無狀態,那就讓服務器和瀏覽器共同維護一個狀態吧!這就是會話機制

2、會話機制

瀏覽器第一次請求服務器,服務器創建一個會話,並將會話的id作爲響應的一部分發送給瀏覽器,瀏覽器存儲會話id,並在後續第二次和第三次請求中帶上會話id,服務器取得請求中的會話id就知道是不是同一個用戶了,這個過程用下圖說明,後續請求與第一次請求產生了關聯

服務器在內存中保存會話對象,瀏覽器怎麼保存會話id呢?你可能會想到兩種方式

  1. 請求參數
  2. cookie

將會話id作爲每一個請求的參數,服務器接收請求自然能解析參數獲得會話id,並藉此判斷是否來自同一會話,很明顯,這種方式不靠譜。那就瀏覽器自己來維護這個會話id吧,每次發送http請求時瀏覽器自動發送會話id,cookie機制正好用來做這件事。cookie是瀏覽器用來存儲少量數據的一種機制,數據以”key/value“形式存儲,瀏覽器發送http請求時自動附帶cookie信息

tomcat會話機制當然也實現了cookie,訪問tomcat服務器時,瀏覽器中可以看到一個名爲“JSESSIONID”的cookie,這就是tomcat會話機制維護的會話id,使用了cookie的請求響應過程如下圖

3、登錄狀態

有了會話機制,登錄狀態就好明白了,我們假設瀏覽器第一次請求服務器需要輸入用戶名與密碼驗證身份,服務器拿到用戶名密碼去數據庫比對,正確的話說明當前持有這個會話的用戶是合法用戶,應該將這個會話標記爲“已授權”或者“已登錄”等等之類的狀態,既然是會話的狀態,自然要保存在會話對象中,tomcat在會話對象中設置登錄狀態如下

HttpSession session = request.getSession();
session.setAttribute("isLogin", true);

用戶再次訪問時,tomcat在會話對象中查看登錄狀態

HttpSession session = request.getSession();
session.getAttribute("isLogin");

實現了登錄狀態的瀏覽器請求服務器模型如下圖描述

每次請求受保護資源時都會檢查會話對象中的登錄狀態,只有 isLogin=true 的會話才能訪問,登錄機制因此而實現。

二、多系統的複雜性

web系統早已從久遠的單系統發展成爲如今由多系統組成的應用羣,面對如此衆多的系統,用戶難道要一個一個登錄、然後一個一個註銷嗎?就像下圖描述的這樣

web系統由單系統發展成多系統組成的應用羣,複雜性應該由系統內部承擔,而不是用戶。無論web系統內部多麼複雜,對用戶而言,都是一個統一的整體,也就是說,用戶訪問web系統的整個應用羣與訪問單個系統一樣,登錄/註銷只要一次就夠了

雖然單系統的登錄解決方案很完美,但對於多系統應用羣已經不再適用了,爲什麼呢?

單系統登錄解決方案的核心是cookie,cookie攜帶會話id在瀏覽器與服務器之間維護會話狀態。但cookie是有限制的,這個限制就是cookie的域(通常對應網站的域名),瀏覽器發送http請求時會自動攜帶與該域匹配的cookie,而不是所有cookie

既然這樣,爲什麼不將web應用羣中所有子系統的域名統一在一個頂級域名下,例如“*.baidu.com”,然後將它們的cookie域設置爲“baidu.com”,這種做法理論上是可以的,甚至早期很多多系統登錄就採用這種同域名共享cookie的方式。

然而,可行並不代表好,共享cookie的方式存在衆多侷限。首先,應用羣域名得統一;其次,應用羣各系統使用的技術(至少是web服務器)要相同,不然cookie的key值(tomcat爲JSESSIONID)不同,無法維持會話,共享cookie的方式是無法實現跨語言技術平臺登錄的,比如java、php、.net系統之間;第三,cookie本身不安全。

因此,我們需要一種全新的登錄方式來實現多系統應用羣的登錄,這就是單點登錄

三、單點登錄

什麼是單點登錄?單點登錄全稱Single Sign On(以下簡稱SSO),是指在多系統應用羣中登錄一個系統,便可在其他所有系統中得到授權而無需再次登錄,包括單點登錄與單點註銷兩部分

1、登錄

相比於單系統登錄,sso需要一個獨立的認證中心,只有認證中心能接受用戶的用戶名密碼等安全信息,其他系統不提供登錄入口,只接受認證中心的間接授權。間接授權通過令牌實現,sso認證中心驗證用戶的用戶名密碼沒問題,創建授權令牌,在接下來的跳轉過程中,授權令牌作爲參數發送給各個子系統,子系統拿到令牌,即得到了授權,可以藉此創建局部會話,局部會話登錄方式與單系統的登錄方式相同。這個過程,也就是單點登錄的原理,用下圖說明

下面對上圖簡要描述

  1. 用戶訪問系統1的受保護資源,系統1發現用戶未登錄,跳轉至sso認證中心,並將自己的地址作爲參數
  2. sso認證中心發現用戶未登錄,將用戶引導至登錄頁面
  3. 用戶輸入用戶名密碼提交登錄申請
  4. sso認證中心校驗用戶信息,創建用戶與sso認證中心之間的會話,稱爲全局會話,同時創建授權令牌
  5. sso認證中心帶着令牌跳轉會最初的請求地址(系統1)
  6. 系統1拿到令牌,去sso認證中心校驗令牌是否有效
  7. sso認證中心校驗令牌,返回有效,註冊系統1
  8. 系統1使用該令牌創建與用戶的會話,稱爲局部會話,返回受保護資源
  9. 用戶訪問系統2的受保護資源
  10. 系統2發現用戶未登錄,跳轉至sso認證中心,並將自己的地址作爲參數
  11. sso認證中心發現用戶已登錄,跳轉回系統2的地址,並附上令牌
  12. 系統2拿到令牌,去sso認證中心校驗令牌是否有效
  13. sso認證中心校驗令牌,返回有效,註冊系統2
  14. 系統2使用該令牌創建與用戶的局部會話,返回受保護資源

用戶登錄成功之後,會與sso認證中心及各個子系統建立會話,用戶與sso認證中心建立的會話稱爲全局會話,用戶與各個子系統建立的會話稱爲局部會話,局部會話建立之後,用戶訪問子系統受保護資源將不再通過sso認證中心,全局會話與局部會話有如下約束關係

  1. 局部會話存在,全局會話一定存在
  2. 全局會話存在,局部會話不一定存在
  3. 全局會話銷燬,局部會話必須銷燬

你可以通過博客園、百度、csdn、淘寶等網站的登錄過程加深對單點登錄的理解,注意觀察登錄過程中的跳轉url與參數

2、註銷

單點登錄自然也要單點註銷,在一個子系統中註銷,所有子系統的會話都將被銷燬,用下面的圖來說明

sso認證中心一直監聽全局會話的狀態,一旦全局會話銷燬,監聽器將通知所有註冊系統執行註銷操作

下面對上圖簡要說明

  1. 用戶向系統1發起註銷請求
  2. 系統1根據用戶與系統1建立的會話id拿到令牌,向sso認證中心發起註銷請求
  3. sso認證中心校驗令牌有效,銷燬全局會話,同時取出所有用此令牌註冊的系統地址
  4. sso認證中心向所有註冊系統發起註銷請求
  5. 各註冊系統接收sso認證中心的註銷請求,銷燬局部會話
  6. sso認證中心引導用戶至登錄頁面

四、部署圖

單點登錄涉及sso認證中心與衆子系統,子系統與sso認證中心需要通信以交換令牌、校驗令牌及發起註銷請求,因而子系統必須集成sso的客戶端,sso認證中心則是sso服務端,整個單點登錄過程實質是sso客戶端與服務端通信的過程,用下圖描述

sso認證中心與sso客戶端通信方式有多種,這裏以簡單好用的httpClient爲例,web service、rpc、restful api都可以

五、實現

只是簡要介紹下基於java的實現過程,不提供完整源碼,明白了原理,我相信你們可以自己實現。sso採用客戶端/服務端架構,我們先看sso-client與sso-server要實現的功能(下面:sso認證中心=sso-server)

sso-client

  1. 攔截子系統未登錄用戶請求,跳轉至sso認證中心
  2. 接收並存儲sso認證中心發送的令牌
  3. 與sso-server通信,校驗令牌的有效性
  4. 建立局部會話
  5. 攔截用戶註銷請求,向sso認證中心發送註銷請求
  6. 接收sso認證中心發出的註銷請求,銷燬局部會話

sso-server

  1. 驗證用戶的登錄信息
  2. 創建全局會話
  3. 創建授權令牌
  4. 與sso-client通信發送令牌
  5. 校驗sso-client令牌有效性
  6. 系統註冊
  7. 接收sso-client註銷請求,註銷所有會話

接下來,我們按照原理來一步步實現sso吧!

1、sso-client攔截未登錄請求

Java攔截請求的方式有servlet、filter、listener三種方式,我們採用filter。在sso-client中新建LoginFilter.java類並實現Filter接口,在doFilter()方法中加入對未登錄用戶的攔截

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse res = (HttpServletResponse) response;
    HttpSession session = req.getSession();
    
    if (session.getAttribute("isLogin")) {
        chain.doFilter(request, response);
        return;
    }
    //跳轉至sso認證中心
    res.sendRedirect("sso-server-url-with-system-url");
}

2、sso-server攔截未登錄請求

攔截從sso-client跳轉至sso認證中心的未登錄請求,跳轉至登錄頁面,這個過程與sso-client完全一樣

3、sso-server驗證用戶登錄信息

用戶在登錄頁面輸入用戶名密碼,請求登錄,sso認證中心校驗用戶信息,校驗成功,將會話狀態標記爲“已登錄”

@RequestMapping("/login")
public String login(String username, String password, HttpServletRequest req) {
    this.checkLoginInfo(username, password);
    req.getSession().setAttribute("isLogin", true);
    return "success";
}

4、sso-server創建授權令牌

授權令牌是一串隨機字符,以什麼樣的方式生成都沒有關係,只要不重複、不易僞造即可,下面是一個例子

String token = UUID.randomUUID().toString();

5、sso-client取得令牌並校驗

sso認證中心登錄後,跳轉回子系統並附上令牌,子系統(sso-client)取得令牌,然後去sso認證中心校驗,在LoginFilter.java的doFilter()中添加幾行

// 請求附帶token參數
String token = req.getParameter("token");
if (token != null) {
    // 去sso認證中心校驗token
    boolean verifyResult = this.verify("sso-server-verify-url", token);
    if (!verifyResult) {
        res.sendRedirect("sso-server-url");
        return;
    }
    chain.doFilter(request, response);
}

verify()方法使用httpClient實現,這裏僅簡略介紹,httpClient詳細使用方法請參考官方文檔

HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token");
HttpResponse httpResponse = httpClient.execute(httpPost);

6、sso-server接收並處理校驗令牌請求

用戶在sso認證中心登錄成功後,sso-server創建授權令牌並存儲該令牌,所以,sso-server對令牌的校驗就是去查找這個令牌是否存在以及是否過期,令牌校驗成功後sso-server將發送校驗請求的系統註冊到sso認證中心(就是存儲起來的意思)

令牌與註冊系統地址通常存儲在key-value數據庫(如redis)中,redis可以爲key設置有效時間也就是令牌的有效期。redis運行在內存中,速度非常快,正好sso-server不需要持久化任何數據。

令牌與註冊系統地址可以用下圖描述的結構存儲在redis中,可能你會問,爲什麼要存儲這些系統的地址?如果不存儲,註銷的時候就麻煩了,用戶向sso認證中心提交註銷請求,sso認證中心註銷全局會話,但不知道哪些系統用此全局會話建立了自己的局部會話,也不知道要向哪些子系統發送註銷請求註銷局部會話

7、sso-client校驗令牌成功創建局部會話

令牌校驗成功後,sso-client將當前局部會話標記爲“已登錄”,修改LoginFilter.java,添加幾行

if (verifyResult) {
    session.setAttribute("isLogin", true);
}

sso-client還需將當前會話id與令牌綁定,表示這個會話的登錄狀態與令牌相關,此關係可以用java的hashmap保存,保存的數據用來處理sso認證中心發來的註銷請求

8、註銷過程

用戶向子系統發送帶有“logout”參數的請求(註銷請求),sso-client攔截器攔截該請求,向sso認證中心發起註銷請求

String logout = req.getParameter("logout");
if (logout != null) {
    this.ssoServer.logout(token);
}

sso認證中心也用同樣的方式識別出sso-client的請求是註銷請求(帶有“logout”參數),sso認證中心註銷全局會話

@RequestMapping("/logout")
public String logout(HttpServletRequest req) {
    HttpSession session = req.getSession();
    if (session != null) {
        session.invalidate();//觸發LogoutListener
    }
    return "redirect:/";
}

sso認證中心有一個全局會話的監聽器,一旦全局會話註銷,將通知所有註冊系統註銷

public class LogoutListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent event) {}
    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        //通過httpClient向所有註冊系統發送註銷請求
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章