必備技能!單點登錄系統原理與實現!

來源 | cnblogs.com/ywlaker/p/6113927.html

一、單系統登錄機制

1、http 無狀態協議

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

使用 Redis 實現一個輕量級的搜索引擎,牛逼啊 !

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

2、會話機制

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

知乎高贊:有一個傻 X 導師是種怎樣的體驗?

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

  1. 請求參數

  2. cookie

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

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

這是我見過最蛋疼的註冊中心與 API 網關實踐!

3、登錄狀態

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

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

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

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

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

B 站面試官炫耀身價過億,貶低北郵應試者引熱議!不知北郵畢業的 B 站創始人作何感想?

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

二、多系統的複雜性

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

MySQL 每秒 57 萬的寫入,帶你裝逼,帶你飛 !!

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

程序員應該知道的 10 大編程格言

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

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

4 款 MySQL Binlog 日誌處理工具對比,誰纔是王者?

既然這樣,爲什麼不將 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、註銷

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

DevOps 和 SRE 的十大開源項目

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

下面對上圖簡要說明

  1. 用戶向系統 1 發起註銷請求

  2. 系統 1 根據用戶與系統 1 建立的會話 id 拿到令牌,向 sso 認證中心發起註銷請求

  3. sso 認證中心校驗令牌有效,銷燬全局會話,同時取出所有用此令牌註冊的系統地址

  4. sso 認證中心向所有註冊系統發起註銷請求

  5. 各註冊系統接收 sso 認證中心的註銷請求,銷燬局部會話

  6. sso 認證中心引導用戶至登錄頁面

四、部署圖

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

使用 IDEA 解決 Java8 的數據流問題,極大提升生產力!!

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向所有註冊系統發送註銷請求
    }
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xUp4TIwyWN2AYkKVzS5Nxw