什么是單點(diǎn)登錄(SSO)

前言

只有光頭才能變強(qiáng)。

文本已收錄至我的GitHub倉庫渗柿,歡迎Star:https://github.com/ZhongFuCheng3y/3y

在我實(shí)習(xí)之前我就已經(jīng)在看單點(diǎn)登錄的是什么了殷费,但是實(shí)習(xí)的時(shí)候一直在忙其他的事勇婴,所以有幾個(gè)網(wǎng)站就一直躺在我的收藏夾里邊:

收藏的一些網(wǎng)站

在前陣子有個(gè)讀者來我這投稿哗讥,是使用JWT實(shí)現(xiàn)單點(diǎn)登錄的(但是文章中并沒有介紹什么是單點(diǎn)登錄)包颁,所以我覺得是時(shí)候來整理一下了瞻想。。

一娩嚼、什么是單點(diǎn)登錄蘑险?

單點(diǎn)登錄的英文名叫做:Single Sign On(簡(jiǎn)稱SSO)。

初學(xué)/以前的時(shí)候岳悟,一般我們就單系統(tǒng)佃迄,所有的功能都在同一個(gè)系統(tǒng)上。

所有的功能都在同一個(gè)系統(tǒng)上

后來贵少,我們?yōu)榱?strong>合理利用資源和降低耦合性呵俏,于是把單系統(tǒng)拆分成多個(gè)子系統(tǒng)。

拆分成多個(gè)子系統(tǒng)

比如阿里系的淘寶和天貓滔灶,很明顯地我們可以知道這是兩個(gè)系統(tǒng)普碎,但是你在使用的時(shí)候,登錄了天貓录平,淘寶也會(huì)自動(dòng)登錄麻车。

登錄了天貓,淘寶也登錄了

簡(jiǎn)單來說斗这,單點(diǎn)登錄就是在多個(gè)系統(tǒng)中动猬,用戶只需一次登錄,各個(gè)系統(tǒng)即可感知該用戶已經(jīng)登錄表箭。

二赁咙、回顧單系統(tǒng)登錄

在我初學(xué)JavaWeb的時(shí)候,登錄和注冊(cè)是我做得最多的一個(gè)功能了(初學(xué)Servlet的時(shí)候做過、學(xué)SpringMVC的時(shí)候做過彼水、跟著做項(xiàng)目的時(shí)候做過…)崔拥,反正我也數(shù)不清我做了多少次登錄和注冊(cè)的功能了...這里簡(jiǎn)單講述一下我們初學(xué)時(shí)是怎么做登錄功能的。

眾所周知猿涨,HTTP是無狀態(tài)的協(xié)議握童,這意味著服務(wù)器無法確認(rèn)用戶的信息。于是乎叛赚,W3C就提出了:給每一個(gè)用戶都發(fā)一個(gè)通行證,無論誰訪問的時(shí)候都需要攜帶通行證稽揭,這樣服務(wù)器就可以從通行證上確認(rèn)用戶的信息俺附。通行證就是Cookie

如果說Cookie是檢查用戶身上的”通行證“來確認(rèn)用戶的身份溪掀,那么Session就是通過檢查服務(wù)器上的”客戶明細(xì)表“來確認(rèn)用戶的身份的事镣。Session相當(dāng)于在服務(wù)器中建立了一份“客戶明細(xì)表”

HTTP協(xié)議是無狀態(tài)的揪胃,Session不能依據(jù)HTTP連接來判斷是否為同一個(gè)用戶璃哟。于是乎:服務(wù)器向用戶瀏覽器發(fā)送了一個(gè)名為JESSIONID的Cookie,它的值是Session的id值喊递。其實(shí)Session是依據(jù)Cookie來識(shí)別是否是同一個(gè)用戶随闪。

所以,一般我們單系統(tǒng)實(shí)現(xiàn)登錄會(huì)這樣做:

  • 登錄:將用戶信息保存在Session對(duì)象中
    • 如果在Session對(duì)象中能查到骚勘,說明已經(jīng)登錄
    • 如果在Session對(duì)象中查不到铐伴,說明沒登錄(或者已經(jīng)退出了登錄)
  • 注銷(退出登錄):從Session中刪除用戶的信息
  • 記住我(關(guān)閉掉瀏覽器后,重新打開瀏覽器還能保持登錄狀態(tài)):配合Cookie來用

我之前Demo的代碼俏讹,可以參考一下:

 /**
 * 用戶登陸
 */
@PostMapping(value = "/user/session", produces = {"application/json;charset=UTF-8"})
public Result login(String mobileNo, String password, String inputCaptcha, HttpSession session, HttpServletResponse response) {

    //判斷驗(yàn)證碼是否正確
    if (WebUtils.validateCaptcha(inputCaptcha, "captcha", session)) {

        //判斷有沒有該用戶
        User user = userService.userLogin(mobileNo, password);
        if (user != null) {
            /*設(shè)置自動(dòng)登陸当宴,一個(gè)星期.  將token保存在數(shù)據(jù)庫中*/
            String loginToken = WebUtils.md5(new Date().toString() + session.getId());
            user.setLoginToken(loginToken);
            User user1 = userService.userUpload(user);

            session.setAttribute("user", user1);

            CookieUtil.addCookie(response,"loginToken",loginToken,604800);

            return ResultUtil.success(user1);

        } else {
            return ResultUtil.error(ResultEnum.LOGIN_ERROR);
        }
    } else {
        return ResultUtil.error(ResultEnum.CAPTCHA_ERROR);
    }

}

/**
 * 用戶退出
 */
@DeleteMapping(value = "/session", produces = {"application/json;charset=UTF-8"})
public Result logout(HttpSession session,HttpServletRequest request,HttpServletResponse response ) {

    //刪除session和cookie
    session.removeAttribute("user");

    CookieUtil.clearCookie(request, response, "loginToken");

    return ResultUtil.success();
}
/**
* @author ozc
* @version 1.0
* <p>
* 攔截器;實(shí)現(xiàn)自動(dòng)登陸功能
*/
public class UserInterceptor implements HandlerInterceptor {


@Autowired
private UserService userService;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
    User sessionUser = (User) request.getSession().getAttribute("user");

    // 已經(jīng)登陸了泽疆,放行
    if (sessionUser != null) {
        return true;
    } else {
        //得到帶過來cookie是否存在
        String loginToken = CookieUtil.findCookieByName(request, "loginToken");
        if (StringUtils.isNotBlank(loginToken)) {
            //到數(shù)據(jù)庫查詢有沒有該Cookie
            User user = userService.findUserByLoginToken(loginToken);
            if (user != null) {
                request.getSession().setAttribute("user", user);
                return true;
            } else {
                //沒有該Cookie與之對(duì)應(yīng)的用戶(Cookie不匹配)
                CookieUtil.clearCookie(request, response, "loginToken");
                return false;
            }
        } else {

            //沒有cookie户矢、也沒有登陸。是index請(qǐng)求獲取用戶信息殉疼,可以放行
            if (request.getRequestURI().contains("session")) {
                return true;
            }

            //沒有cookie憑證
            response.sendRedirect("/login.html");
            return false;
        }
    }
}
}

總結(jié)一下上面代碼的思路:

  • 用戶登錄時(shí)梯浪,驗(yàn)證用戶的賬戶和密碼
  • 生成一個(gè)Token保存在數(shù)據(jù)庫中,將Token寫到Cookie中
  • 將用戶數(shù)據(jù)保存在Session中
  • 請(qǐng)求時(shí)都會(huì)帶上Cookie株依,檢查有沒有登錄驱证,如果已經(jīng)登錄則放行

如果沒看懂的同學(xué),建議回顧Session和Cookie和HTTP:

三吻育、多系統(tǒng)登錄的問題與解決

3.1 Session不共享問題

單系統(tǒng)登錄功能主要是用Session保存用戶信息來實(shí)現(xiàn)的念秧,但我們清楚的是:多系統(tǒng)即可能有多個(gè)Tomcat,而Session是依賴當(dāng)前系統(tǒng)的Tomcat布疼,所以系統(tǒng)A的Session和系統(tǒng)B的Session是不共享的摊趾。

系統(tǒng)A的Session和系統(tǒng)B的Session是不共享的

解決系統(tǒng)之間Session不共享問題有一下幾種方案:

  • Tomcat集群Session全局復(fù)制(集群內(nèi)每個(gè)tomcat的session完全同步)【會(huì)影響集群的性能呢,不建議】
  • 根據(jù)請(qǐng)求的IP進(jìn)行Hash映射到對(duì)應(yīng)的機(jī)器上(這就相當(dāng)于請(qǐng)求的IP一直會(huì)訪問同一個(gè)服務(wù)器)【如果服務(wù)器宕機(jī)了游两,會(huì)丟失了一大部分Session的數(shù)據(jù)砾层,不建議】
  • 把Session數(shù)據(jù)放在Redis中(使用Redis模擬Session)【建議
    • 如果還不了解Redis的同學(xué),建議移步(Redis合集

我們可以將登錄功能單獨(dú)抽取出來贱案,做成一個(gè)子系統(tǒng)肛炮。

抽取出來成為子系統(tǒng)

SSO(登錄系統(tǒng))的邏輯如下:

// 登錄功能(SSO單獨(dú)的服務(wù))
@Override
public TaotaoResult login(String username, String password) throws Exception {
    
    //根據(jù)用戶名查詢用戶信息
    TbUserExample example = new TbUserExample();
    Criteria criteria = example.createCriteria();
    criteria.andUsernameEqualTo(username);
    List<TbUser> list = userMapper.selectByExample(example);
    if (null == list || list.isEmpty()) {
        return TaotaoResult.build(400, "用戶不存在");
    }
    //核對(duì)密碼
    TbUser user = list.get(0);
    if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) {
        return TaotaoResult.build(400, "密碼錯(cuò)誤");
    }
    //登錄成功,把用戶信息寫入redis
    //生成一個(gè)用戶token
    String token = UUID.randomUUID().toString();
    jedisCluster.set(USER_TOKEN_KEY + ":" + token, JsonUtils.objectToJson(user));
    //設(shè)置session過期時(shí)間
    jedisCluster.expire(USER_TOKEN_KEY + ":" + token, SESSION_EXPIRE_TIME);
    return TaotaoResult.ok(token);
}

其他子系統(tǒng)登錄時(shí)宝踪,請(qǐng)求SSO(登錄系統(tǒng))進(jìn)行登錄侨糟,將返回的token寫到Cookie中,下次訪問時(shí)則把Cookie帶上:

public TaotaoResult login(String username, String password, 
        HttpServletRequest request, HttpServletResponse response) {
    //請(qǐng)求參數(shù)
    Map<String, String> param = new HashMap<>();
    param.put("username", username);
    param.put("password", password);
    //登錄處理
    String stringResult = HttpClientUtil.doPost(REGISTER_USER_URL + USER_LOGIN_URL, param);
    TaotaoResult result = TaotaoResult.format(stringResult);
    //登錄出錯(cuò)
    if (result.getStatus() != 200) {
        return result;
    }
    //登錄成功后把取token信息瘩燥,并寫入cookie
    String token = (String) result.getData();
    //寫入cookie
    CookieUtils.setCookie(request, response, "TT_TOKEN", token);
    //返回成功
    return result;
    
}

總結(jié):

  • SSO系統(tǒng)生成一個(gè)token秕重,并將用戶信息存到Redis中,并設(shè)置過期時(shí)間
  • 其他系統(tǒng)請(qǐng)求SSO系統(tǒng)進(jìn)行登錄颤芬,得到SSO返回的token悲幅,寫到Cookie中
  • 每次請(qǐng)求時(shí),Cookie都會(huì)帶上站蝠,攔截器得到token汰具,判斷是否已經(jīng)登錄

到這里,其實(shí)我們會(huì)發(fā)現(xiàn)其實(shí)就兩個(gè)變化:

  • 將登陸功能抽取為一個(gè)系統(tǒng)(SSO)菱魔,其他系統(tǒng)請(qǐng)求SSO進(jìn)行登錄
  • 本來將用戶信息存到Session留荔,現(xiàn)在將用戶信息存到Redis

3.2 Cookie跨域的問題

上面我們解決了Session不能共享的問題,但其實(shí)還有另一個(gè)問題澜倦。Cookie是不能跨域的

比如說聚蝶,我們請(qǐng)求<https://www.google.com/>時(shí),瀏覽器會(huì)自動(dòng)把google.com的Cookie帶過去給google的服務(wù)器藻治,而不會(huì)把<https://www.baidu.com/>的Cookie帶過去給google的服務(wù)器碘勉。

這就意味著,由于域名不同桩卵,用戶向系統(tǒng)A登錄后验靡,系統(tǒng)A返回給瀏覽器的Cookie倍宾,用戶再請(qǐng)求系統(tǒng)B的時(shí)候不會(huì)將系統(tǒng)A的Cookie帶過去。

針對(duì)Cookie存在跨域問題胜嗓,有幾種解決方案:

  1. 服務(wù)端將Cookie寫到客戶端后高职,客戶端對(duì)Cookie進(jìn)行解析,將Token解析出來辞州,此后請(qǐng)求都把這個(gè)Token帶上就行了
  2. 多個(gè)域名共享Cookie怔锌,在寫到客戶端的時(shí)候設(shè)置Cookie的domain。
  3. 將Token保存在SessionStroage中(不依賴Cookie就沒有跨域的問題了)

到這里变过,我們已經(jīng)可以實(shí)現(xiàn)單點(diǎn)登錄了埃元。

3.3 CAS原理

說到單點(diǎn)登錄,就肯定會(huì)見到這個(gè)名詞:CAS (Central Authentication Service)媚狰,下面說說CAS是怎么搞的亚情。

如果已經(jīng)將登錄單獨(dú)抽取成系統(tǒng)出來,我們還能這樣玩」現(xiàn)在我們有兩個(gè)系統(tǒng),分別是www.java3y.comwww.java4y.com衫生,一個(gè)SSOwww.sso.com

現(xiàn)在我們有三個(gè)系統(tǒng)

首先裳瘪,用戶想要訪問系統(tǒng)Awww.java3y.com受限的資源(比如說購(gòu)物車功能,購(gòu)物車功能需要登錄后才能訪問)罪针,系統(tǒng)Awww.java3y.com發(fā)現(xiàn)用戶并沒有登錄彭羹,于是重定向到sso認(rèn)證中心,并將自己的地址作為參數(shù)泪酱。請(qǐng)求的地址如下:

  • www.sso.com?service=www.java3y.com

sso認(rèn)證中心發(fā)現(xiàn)用戶未登錄派殷,將用戶引導(dǎo)至登錄頁面,用戶進(jìn)行輸入用戶名和密碼進(jìn)行登錄墓阀,用戶與認(rèn)證中心建立全局會(huì)話(生成一份Token毡惜,寫到Cookie中,保存在瀏覽器上)

4步過程

隨后斯撮,認(rèn)證中心重定向回系統(tǒng)A经伙,并把Token攜帶過去給系統(tǒng)A,重定向的地址如下:

  • www.java3y.com?token=xxxxxxx

接著勿锅,系統(tǒng)A去sso認(rèn)證中心驗(yàn)證這個(gè)Token是否正確帕膜,如果正確,則系統(tǒng)A和用戶建立局部會(huì)話(創(chuàng)建Session)溢十。到此垮刹,系統(tǒng)A和用戶已經(jīng)是登錄狀態(tài)了。

第五步和第六步

此時(shí)张弛,用戶想要訪問系統(tǒng)Bwww.java4y.com受限的資源(比如說訂單功能荒典,訂單功能需要登錄后才能訪問)酪劫,系統(tǒng)Bwww.java4y.com發(fā)現(xiàn)用戶并沒有登錄,于是重定向到sso認(rèn)證中心种蝶,并將自己的地址作為參數(shù)契耿。請(qǐng)求的地址如下:

  • www.sso.com?service=www.java4y.com

注意,因?yàn)橹坝脩襞c認(rèn)證中心www.sso.com已經(jīng)建立了全局會(huì)話(當(dāng)時(shí)已經(jīng)把Cookie保存到瀏覽器上了)螃征,所以這次系統(tǒng)B重定向到認(rèn)證中心www.sso.com是可以帶上Cookie的搪桂。

認(rèn)證中心根據(jù)帶過來的Cookie發(fā)現(xiàn)已經(jīng)與用戶建立了全局會(huì)話了,認(rèn)證中心重定向回系統(tǒng)B盯滚,并把Token攜帶過去給系統(tǒng)B踢械,重定向的地址如下:

  • www.java4y.com?token=xxxxxxx

接著,系統(tǒng)B去sso認(rèn)證中心驗(yàn)證這個(gè)Token是否正確魄藕,如果正確内列,則系統(tǒng)B和用戶建立局部會(huì)話(創(chuàng)建Session)。到此背率,系統(tǒng)B和用戶已經(jīng)是登錄狀態(tài)了话瞧。

系統(tǒng)B的流程圖

看到這里,其實(shí)SSO認(rèn)證中心就類似一個(gè)中轉(zhuǎn)站寝姿。

參考資料:

最后

樂于輸出干貨的Java技術(shù)公眾號(hào):Java3y交排。公眾號(hào)內(nèi)有200多篇原創(chuàng)技術(shù)文章、海量視頻資源饵筑、精美腦圖埃篓,關(guān)注即可獲取根资!

轉(zhuǎn)發(fā)到朋友圈是對(duì)我最大的支持架专!

覺得我的文章寫得不錯(cuò),點(diǎn)玄帕!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末部脚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子桨仿,更是在濱河造成了極大的恐慌睛低,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件服傍,死亡現(xiàn)場(chǎng)離奇詭異钱雷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吹零,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門罩抗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灿椅,你說我怎么就攤上這事套蒂〕В” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵操刀,是天一觀的道長(zhǎng)烁挟。 經(jīng)常有香客問我,道長(zhǎng)骨坑,這世上最難降的妖魔是什么撼嗓? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮欢唾,結(jié)果婚禮上且警,老公的妹妹穿的比我還像新娘。我一直安慰自己礁遣,他們只是感情好斑芜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祟霍,像睡著了一般杏头。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沸呐,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天大州,我揣著相機(jī)與錄音,去河邊找鬼垂谢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疮茄,可吹牛的內(nèi)容都是我干的滥朱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼力试,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼徙邻!你這毒婦竟也來了始腾?” 一聲冷哼從身側(cè)響起贬丛,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捡偏,沒想到半個(gè)月后怖糊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帅容,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年伍伤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了并徘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扰魂,死狀恐怖麦乞,靈堂內(nèi)的尸體忽然破棺而出蕴茴,到底是詐尸還是另有隱情,我是刑警寧澤姐直,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布倦淀,位于F島的核電站,受9級(jí)特大地震影響声畏,放射性物質(zhì)發(fā)生泄漏撞叽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一砰识、第九天 我趴在偏房一處隱蔽的房頂上張望能扒。 院中可真熱鬧,春花似錦辫狼、人聲如沸初斑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽见秤。三九已至,卻和暖如春真椿,著一層夾襖步出監(jiān)牢的瞬間鹃答,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工突硝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留测摔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓解恰,卻偏偏與公主長(zhǎng)得像锋八,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子护盈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355