用上帝視角俯瞰單點登錄的前世與今生(內含兩種實現方式的源碼)

目錄

 

1.什麼是單點登錄?

2.單點登錄的前世與今生——前世篇(SSO)

2.1.單點登錄的定義是:

2.2.總結成簡單一句話說就是:

2.3.單點登錄的優點:

2.4.拿去遊樂園買票來講一下單點登錄的簡單實現機制:

2.5.單點登錄的實現機制:

2.6.單點登錄的分類:

2.7.同域SSO:

2.7.1.同域SSO概念:

2.7.2.同父域SSO概念:

2.7.3.同域SSO實現流程圖:

2.7.4.代碼實現同域SSO:

3.單點登錄的前世與今生——今生篇(CAS)

3.1.同域SSO

3.1.1.CAS

3.1.2.CAS包含兩個部分:

3.1.3.在說跨域SSO流程之前要提前說幾個CAS知識點:

3.1.4.理解幾種票根:

3.1.5.跨域SSO實現流程圖:

3.1.6.代碼實現跨域SSO(基於CAS-Client):


1.什麼是單點登錄?

        大家都知道百度不僅僅只有搜索引擎這一個業務,它還有百度貼吧,百度雲盤等衆所周知的業務。而且有沒有發現?你只需要在百度任意一個業務中登陸過,其他業務都默認自動登錄了你這個賬號呢?比如你在百度搜索中登錄了你的賬號,你再進入百度貼吧或者百度網盤,顯示你已經登錄了。

百度首頁登錄:

在登錄這個“進階的小名”賬號的情況下,分別打開百度網盤和百度地圖的效果:

       百度網盤和百度地圖都默認登錄了“進階的小名”這個賬號了。相對的,如果你在任何一個業務中退出登錄,其他的業務也會自動退出當前賬號的登錄。

2.單點登錄的前世與今生——前世篇(SSO)

2.1.單點登錄的定義是:

       單點登錄(英語:Single sign-on,縮寫爲 SSO),又譯爲單一簽入,一種對於許多相互關連,但是又是各自獨立的軟件系統,提供訪問控制的屬性。當擁有這項屬性時,當用戶登錄時,就可以獲取所有系統的訪問權限,不用對每個單一系統都逐一登錄。這項功能通常是以輕型目錄訪問協議(LDAP)來實現,在服務器上會將用戶信息存儲到LDAP數據庫中。相同的,單一退出(single sign-off)就是指,只需要單一的退出動作,就可以結束對於多個系統的訪問權限。

2.2.總結成簡單一句話說就是:

在多個應用系統中,用戶只需要登錄一次就可以訪問相互信任的應用系統。

2.3.單點登錄的優點:

       不管是百度還是我們自己公司的項目,隨着時間的推移,我們都需要根據市場需求或是客戶新的需求退出新的功能,不可能在每一個新加的模塊中都寫一遍登錄的業務,於是單點登錄解決了這個問題,將登錄模塊從其他業務模塊中抽離出來。

2.4.拿去遊樂園買票來講一下單點登錄的簡單實現機制:

       我們95後的天津孩子應該都去過一個叫“樂園”的遊樂場,這個遊樂場裏面有:“碰碰車”、“旋轉木馬”、“叢林鼠”、“摩天輪”等遊樂設施,這麼多的遊樂設施,如果家長想帶着熊孩子挨個兒玩一遍是不太可能了,爲啥啊?還不是因爲排隊買票浪費時間嗎!人多的時候,玩兒倆設施就家走了😂。排一個小時“碰碰車”的隊,孩子進去玩10分鐘出來,孩子是美了,一看,家大人又去旁邊“激流勇進”排隊去了,針對這種熊孩子,“樂園”就推出了通票這一制度。通票就是:“樂園”大門口買一張通票(阿姨給你手腕戴上一個印着通票蓋着戳的這麼一個紙圈),進到裏面家大人就不用單獨給熊孩子排隊挨個設施買票了,家大人找個樹蔭下一呆,看着熊孩子帶着那個紙圈滿園子跑就完了😂。

2.5.單點登錄的實現機制:

如下圖所示,當用戶第一次訪問系統1的時候,因爲還沒有登錄,會被引導到認證系統中進行登錄。

  1. 根據用戶提供的登錄信息,認證系統進行身份效驗,如果通過效驗,應該返回給用戶一個認證的憑據Ticket;
  2. 用戶再訪問其他相互信任的應用的時候,就會把這個Ticket作爲自己認證的憑據,應用系統接受到請求之後會把 Ticket送到認證系統進行效驗,檢查 Ticket的合法性;
  3. 如果通過效驗,用戶就可以在不用再次登錄的情況下訪問系統2和系統3了。

2.6.單點登錄的分類:

我將會把單點登錄分爲“同域SSO”和“跨域SSO”兩部分說:(“跨域SSO” 會在後文 單點登錄的前世與今生——今生篇(CAS)中提到)

2.7.同域SSO:

同域SSO又分爲 : “同域SSO” 和 “同父域SSO”

2.7.1.同域SSO概念:

沒有設置獨立的 SSO 服務器,因爲業務後臺服務器本身就足以承擔 SSO 的職能。

2.7.2.同父域SSO概念:

和同域SSO不同在於,服務器在返回 cookie 的時候,要把cookie 的 domain 設置爲其父域。

2.7.3.同域SSO實現流程圖:

 

同域SSO:

  1. 用戶點擊http://www.xiaoming.com頁面登錄按鈕,向後臺服務器發送登錄請求;
  2. 輸入正確的用戶名密碼,登錄認證成功,服務器會把登錄信息寫入session;
  3. 服務器爲該用戶生成一個cookie,並加入到response header中,隨着請求返回而寫入瀏覽器中;
  4. 用戶再次訪問同域的http://cart.xiaoming.com的時候,瀏覽器會帶上之前的cookie;
  5. 後臺服務器通過該cookie驗證當前賬戶的登陸狀態了。

同父域SSO:

  1. 同父域 SSO 是同域 SSO 的簡單升級,唯一的不同在於,服務器在返回 cookie 的時候,要把cookie 的 domain 設置爲其父域。
  2. 比如兩個產品的地址分別爲 http://www.xiaoming.comhttp://cart.xiaoming.com,那麼 cookie 的域設置爲 xiaoming.com 即可。在訪問兩個資源的時候,這個 cookie 都能發送到服務器,本質上和同域 SSO 沒有區別。

2.7.4.代碼實現同域SSO:

       接下來我通過一個Demo來演示同域SSO的要整過程。這個Demo模擬的是利用cookie實現的同域單點登錄:(因爲我的前後端打包部署在一個Server裏,而且瀏覽器用同一個Origin請求前端和服務器端,即使端口號不同,也不存在跨域問題。)

      先上效果:

 

我在Hosts文件裏面進行了如下配置(模擬):

127.0.0.1    www.xiaoming.com

127.0.0.1    vip.xiaoming.com

127.0.0.1    cart.xiaoming.com

127.0.0.1    login.xiaoming.com

本文只展示部分重要代碼(點擊獲取同域SSO源碼(導入項目時注意:本項目是Gradle構建的多模塊項目))

登錄模塊後端處理登錄業務代碼:

@Controller
@RequestMapping("/login")
public class LoginController {
    //    模擬數據庫用戶數據
    public static Set<User> dbUser;

    static {
        dbUser = new HashSet<>();
        dbUser.add(new User(0, "zhangsan", "1"));
        dbUser.add(new User(1, "lisi", "2"));
        dbUser.add(new User(2, "wangwu", "3"));
    }

    //處理“/login”下的post請求
    @PostMapping
    public String doLogin(User user, HttpSession session, HttpServletResponse response) {
        System.out.println(dbUser);
        String target = (String) session.getAttribute("target");
        System.out.println(target);
        /**
         * 此處爲lambda表達式(源碼中又完整解釋)
         */
        Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
                dbUser.getPassword().equals(user.getPassword()))
                .findFirst();

        //判斷用戶是否登錄
        if (first.isPresent()) {
            //保存用戶登錄信息
            String token = UUID.randomUUID().toString();//隨機生成一個token,UUID(全局唯一標識符)
            Cookie cookie = new Cookie("TOKEN", token);
            //cookie要在子系統之間互相訪問,要在同一個域下
            cookie.setDomain("xiaoming.com");
            response.addCookie(cookie);
            //模擬緩存,把User和隨機生成的cookie存(put)到“loginUser”方法中的map
            LoginCacheUtil.loginUser.put(token, first.get());
        } else {
            //登陸失敗
            session.setAttribute("msg", "用戶名或密碼錯誤");
            return "login";
        }

        //重定向到target地址
        return "redirect:" + target;
    }

    /**
     * 給其他子系統開放一個接口,通過token獲取登錄用戶的信息
     * @param token
     * @return
     */
    @GetMapping("/info")
    @ResponseBody
    //ResponseEntity <T>  ,泛型T 表示要設置的返回的 響應體
    public ResponseEntity<User> getUserInfo(String token) {
        if (!StringUtils.isEmpty(token)) {
            User user = LoginCacheUtil.loginUser.get(token);
            return ResponseEntity.ok(user);
        } else {
            //錯誤請求(我要token,你卻沒給我)
            return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
        }
    }
}

 登錄模塊後端跳轉頁面控制器:

/**
 * 頁面跳轉邏輯
 */
@Controller
@RequestMapping("/view")
public class ViewController {

    /**
     * 跳轉到登錄頁面
     * @return
     * @RequestParam中 value表示參數名字  required表示是否爲必需,defaultValue表示默認值
     * @CookieValue中 有cookie就獲取“TOKEN”沒有就不獲取,cookie不是必須的
     */
    @GetMapping("/login")
    public String tologin(@RequestParam(required = false, defaultValue = "") String target,
                          HttpSession session,
                          @CookieValue(required = false, value = "TOKEN") Cookie cookie) {
        /**
         * 判斷target是否爲空
         * 爲空:主頁面
         * 不爲空:存到session
         */
        if (StringUtils.isEmpty(target)) {
            target = "http://www.xiaoming.com:9010";
        }
        if (cookie != null) {
            /**
             * 如果是登錄的用戶再次訪問登陸系統時,就重定向主頁面(target)
             */
            String value = cookie.getValue();
            //獲取模擬緩存類LoginCacheUtil中拿出對應token的用戶信息
            User user = LoginCacheUtil.loginUser.get(value);
            if (user != null) {
                return "redirect:" + target;
            }
        }
//        else {
        //重定向地址
        session.setAttribute("target", target);
        return "login";
//        }
    }
}

 首頁後端跳轉頁面控制器:

@Controller
@RequestMapping("/view")
public class ViewController {

    @Autowired
    private RestTemplate restTemplate;

    private final String LOGIN_INFO_ADDRESS = "http://login.xiaoming.com:9000/login/info?token=";

    @GetMapping("/index")
    public String toIndex(@CookieValue(required = false, value = "TOKEN") Cookie cookie, HttpSession session) {
        if (cookie != null) {
            String token = cookie.getValue();
            if (!StringUtils.isEmpty(token)) {
                Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
                System.out.println("hahha" + result);
                session.setAttribute("loginUser", result);
            }
        }
        return "/index";
    }
}

文章篇幅有限,只展示部分代碼,可以 點擊獲取同域SSO源碼 clone到本地自行測試。

3.單點登錄的前世與今生——今生篇(CAS)

3.1.跨域SSO

3.1.1.CAS概念:

集中式認證服務(英語:Central Authentication Service,縮寫CAS)是一種針對萬維網的單點登錄協議。它的目的是允許一個用戶訪問多個應用程序,而只需向認證服務器提供一次憑證(如用戶名和密碼)。這樣用戶不僅不需在登陸web應用程序時重複認證,而且這些應用程序也無法獲得密碼等敏感信息。CAS 是 Yale 大學發起的一個企業級的、開源的項目,旨在爲 Web 應用系統提供一種可靠的單點登錄解決方法。

3.1.2.CAS包含兩個部分:

CAS Server 和 CAS Client

  1. CAS Server:負責完成對用戶的認證工作 , 需要獨立部署。
  2. CAS Client:負責處理對客戶端受保護資源的訪問請求,需要對請求方進行身份認證時,重定向到 CAS Server 進行認證。

3.1.3.在說跨域SSO流程之前要提前說幾個CAS知識點:

Cas-Client接口方面:

/login:登錄接口,用於登錄到中心服務器。

/logout:登出接口,用於從中心服務器登出。

3.1.4.理解幾種票根:

1.TGT (Ticket Grangting Ticket) :

    TGT 是 CAS 爲用戶簽發的登錄票據,擁有了 TGT,用戶就可以證明自己在 CAS 成功登錄過。TGT 封裝了 Cookie 值以及此 Cookie 值對應的用戶信息。

2.TGC(Ticket Granting Cookie) :

    CAS Server 生成TGT放入自己的 Session 中,而 TGC 就是這個 Session 的唯一標識(SessionId),以 Cookie 形式放到瀏覽器端。

3.ST(Service Ticket) :

    ST 是 CAS 爲用戶簽發的訪問某一 service 的票據。用戶訪問 service 時,service 發現用戶沒有 ST,則要求用戶去 CAS 獲取 ST。

票據關係:用戶信息簽發TGT,TGT簽發ST,PGT簽發PT。

3.1.5.跨域SSO實現流程圖:

 

  1. 用戶訪問產品 CasClientOne,域名是 http://localhost:8081
  2. 由於用戶沒有攜帶在CasClientOne上登錄的 cookie,所以 CasClientOne重定向到SSO 服務器的地址。
  3. 由於用戶沒有攜帶在 SSO 服務器上登錄的 TGC,所以 SSO 服務器判斷用戶未登錄,給用戶顯示統一登錄界面。
  4. 登錄成功後,SSO 服務器構建用戶在 SSO 登錄的 TGT,同時返回一個 http 重定向(包含 SSO 服務器派發的 ST )。
  5. 重定向的 http response 中包含寫 cookie。這個 cookie 代表用戶在 SSO 中的登錄狀態,它的值是 TGC。
  6. 瀏覽器重定向到CasClientOne。此時重定向的 url 中攜帶着 SSO 服務器生成的 ST。根據 ST,CasClientOne向 SSO 服務器發送請求,SSO 服務器驗證票據的有效性。驗證成功後,CasClientOne知道用戶已經在 SSO服務器登錄了,於是CasClientOne構建用戶登錄 session。
  7. 用戶訪問產品CasClientTWO,域名是  http://localhost:8082
  8. 由於用戶沒有攜帶在CasClientTWO上登錄的cookie,所以 CasClientTWO重定向到SSO 服務器,去詢問用戶在 SSO 中的登錄狀態。
  9. 瀏覽器重定向到 SSO服務器。由於已經向瀏覽器寫入了攜帶 TGC 的cookie,所以此時 SSO 服務器可以拿到,根據 TGC 去查找 TGT,如果找到,就判斷用戶已經在SSO服務器登錄過了。
  10. SSO 服務器返回一個重定向,重定向攜帶 ST。
  11. 瀏覽器帶 ST 重定向到CasClientTWO。
  12. CasClientTWO根據票據向 SSO 服務器發送請求,票據驗證通過後,CasClientTWO知道用戶已經在SSO服務器登錄了,於是生成 session,向瀏覽器寫入CasClientTWO的 cookie。

3.1.6.代碼實現跨域SSO(基於CAS-Client):

先上效果:

本文只展示部分重要代碼(點擊獲取CAS-Demo源碼(導入項目時注意:本項目是Maven構建的多模塊項目))

客戶端One的部分實現:

public class CasClientOne {

    @RequestMapping("/CasClientOne")
    public String CasClientOne(HttpSession session, HttpServletRequest request){

        Principal userPrincipal = request.getUserPrincipal();

        String name = userPrincipal.getName();

        session.setAttribute("msg",name+"登錄了CasClientOne。。。");
        return "CasClientOne";
    }

}

代碼使用Springboot集成Cas5.3實現,點擊獲取配置好的Cas-Server,篇幅有限,本文中只展示了一個客戶端的實現,可以 點擊獲取CAS-Demo源碼 Clone到本地自行測試。

       關於Cas,後期還會更新一些我在公司項目中使用的(SpringBoot CAS-Client)實際遇到的一些問題的解決方案。感興趣的小夥伴可以點點關注。覺得本文對您理解單點登錄有一些幫助的小夥伴,麻煩給這篇文章點點贊。如果大佬發現本文理解存在問題,希望可以在下方評論區指點我改正。最後謝謝大家的閱讀!小名祝大家每天都開開心心噠~😁

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