vue實現單點登入的N種方式

林恆發表於2021-08-03

最近專案停工了,RageFrame的學習暫時告一段落,這一篇給大家分享下有關單點登入的相關知識,並提供一些demo給大家參考,希望對想了解的朋友有一些幫助。

話不多說,先上原理(借鑑地址:https://www.jianshu.com/p/613e44d4a464)

點登入SSO(Single Sign On)說得簡單點就是在一個多系統共存的環境下,使用者在一處登入後,就不用在其他系統中登入,也就是使用者的一次登入能得到其他所有系統的信任。單點登入在大型網站裡使用得非常頻繁,例如像阿里巴巴這樣的網站,在網站的背後是成百上千的子系統,使用者一次操作或交易可能涉及到幾十個子系統的協作,如果每個子系統都需要使用者認證,不僅使用者會瘋掉,各子系統也會為這種重複認證授權的邏輯搞瘋掉。實現單點登入說到底就是要解決如何產生和儲存那個信任,再就是其他系統如何驗證這個信任的有效性,因此要點也就以下兩個:

  • 儲存信任

  • 驗證信任

如果一個系統做到了開頭所講的效果,也就算單點登入,單點登入有不同的實現方式,本文就列出我開發中所遇見過的實現方式。

方法一:以Cookie作為憑證媒介

最簡單的單點登入實現方式,是使用cookie作為媒介,存放使用者憑證。
使用者登入父應用之後,應用返回一個加密的cookie,當使用者訪問子應用的時候,攜帶上這個cookie,授權應用解密cookie並進行校驗,校驗通過則登入當前使用者。

Auth via cookie

不難發現以上方式把信任儲存在客戶端的Cookie中,這種方式很容易令人質疑:

  • Cookie不安全

  • 不能跨域實現免登

對於第一個問題,通過加密Cookie可以保證安全性,當然這是在原始碼不洩露的前提下。如果Cookie的加密演算法洩露,攻擊者通過偽造Cookie則可以偽造特定使用者身份,這是很危險的。
對於第二個問題,更是硬傷。

方法二:通過JSONP實現

對於跨域問題,可以使用JSONP實現。
使用者在父應用中登入後,跟Session匹配的Cookie會存到客戶端中,當使用者需要登入子應用的時候,授權應用訪問父應用提供的JSONP介面,並在請求中帶上父應用域名下的Cookie,父應用接收到請求,驗證使用者的登入狀態,返回加密的資訊,子應用通過解析返回來的加密資訊來驗證使用者,如果通過驗證則登入使用者。

Auth via jsonp

這種方式雖然能解決跨域問題,但是安全性其實跟把信任儲存到Cookie是差不多的。如果一旦加密演算法洩露了,攻擊者可以在本地建立一個實現了登入介面的假冒父應用,通過繫結Host來把子應用發起的請求指向本地的假冒父應用,並作出回應。
因為攻擊者完全可以按照加密演算法來偽造響應請求,子應用接收到這個響應之後一樣可以通過驗證,並且登入特定使用者。

方法三:通過頁面重定向的方式

最後一種介紹的方式,是通過父應用和子應用來回重定向中進行通訊,實現資訊的安全傳遞。
父應用提供一個GET方式的登入介面,使用者通過子應用重定向連線的方式訪問這個介面,如果使用者還沒有登入,則返回一個的登入頁面,使用者輸入賬號密碼進行登入。如果使用者已經登入了,則生成加密的Token,並且重定向到子應用提供的驗證Token的介面,通過解密和校驗之後,子應用登入當前使用者。

Auth via redirect

這種方式較前面兩種方式,接解決了上面兩種方法暴露出來的安全性問題和跨域的問題,但是並沒有前面兩種方式方便。
安全與方便,本來就是一對矛盾。

方法四:使用獨立登入系統

一般說來,大型應用會把授權的邏輯與使用者資訊的相關邏輯獨立成一個應用,稱為使用者中心。
使用者中心不處理業務邏輯,只是處理使用者資訊的管理以及授權給第三方應用。第三方應用需要登入的時候,則把使用者的登入請求轉發給使用者中心進行處理,使用者處理完畢返回憑證,第三方應用驗證憑證,通過後就登入使用者。

 

以上,就是我瞭解的單點登入的給個模式及原理,下面給大家上實戰程式碼,這裡我列舉兩種情況,分類給大家講解和提供我對應的demo(以下理論參考https://www.cnblogs.com/tibos/p/5354000.html)

環境1:a.xxx.com需要跟b.xxx.com實現跨域,這種比較簡單,只需要設定cookie的域名關聯域就可以了 cookie.Domain = "xxx.com",這樣兩個域名間的cookie就可以互相訪問,實現跨域.

demo地址展示:

系統一:sso1.linheng.xyz

系統二:sso2.linheng.xyz

vue具體程式碼:

先打入指令安裝js-cookie

npm i js-cookie -S

然後寫入登入頁面

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <button @click="handleLogin">點選登入</button>
    <button @click="rethome">返回首頁</button>
  </div>
</template>

<script>
import Cookies from 'js-cookie'
export default {
  name: 'home',
  data () {
    return {
      msg: '系統一登入頁面'
    }
  },
  methods: {
		handleLogin() {
        var token = this.randomString();
        Cookies.set('app.token',token, { expires: 1000, path: '/', domain: '.**.com' })//這裡換你的網站根目錄
        Cookies.set('app.loginname','系統一', { expires: 1000, path: '/', domain: '.**.com' })//這裡換你的網站根目錄
        this.$router.push("/"); 
    },
    rethome(){
      this.$router.push("/"); 
    },
    randomString(e) {
        e = e || 32;
        var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
        a = t.length,
        n = "";
        for (var i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));
        return n
    }
	}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

接下來是首頁:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <h3>使用者資訊為:{{token}}</h3>
    <h3>登入地點:{{loginname}}</h3>
    <button @click="logout">登出</button>
  </div>
</template>

<script>
import Cookies from 'js-cookie'

export default {
  name: 'home',
  data () {
    return {
      msg: '系統一主頁面',
      token:'',
      loginname:''
    }
  },
  mounted(){
    const token = Cookies.get('app.token');
    this.token = token;
    const loginname = Cookies.get('app.loginname');
    this.loginname = loginname;
    console.log(token);
    if(!token){
      this.$router.push("/login");
    }
  },
  methods: {
    logout(){
         Cookies.set('app.token','', { expires: 1000, path: '/', domain: '.**.com' })//這裡換你的網站根目錄
        Cookies.set('app.loginname','', { expires: 1000, path: '/', domain: '.**.com' })//這裡換你的網站根目錄
        this.$router.go(0)
    }
	}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

系統二的對應頁面也只是把這兩個頁面弄過去,改下文字方便識別而已。

寫到這裡,大家對於這個的思路已經比較清晰了,如果需要優化,我這邊建議大家把判斷和獲取的方法統一弄成控制元件,然後在router裡進行操作,這樣會更好。

 

環境2:a.aaa.com需要跟b.bbb.com實現跨域,這種不同域名的情況下,想要實現就必須換種方式了.

這個我還沒有寫好demo,這個給大家提供我找到的最靠譜的思路及demo

在這裡我將引入第三者,s.sss.com這個站點,就是某個瀏覽器同時開啟了這3個站點,我們訪問A站點,先判斷自身是否登入,如果session為空,就重定向到S站點,判斷S站點上面是否有cookie,如果S站點上面也沒有cookie,則由S站點重定向到A站點的登入頁.

 這樣我們就實現了第一步,S站做的的就是隱藏在幕後,子站先判斷自己是否存在session,如果不存在,就重定向到主站S上面去驗證.

第二步,驗證登入資訊合法性.這裡我引入token(令牌),網上有很多資料,描述token的傳遞,工作方式是這樣,A登入成功,儲存自身的session,重定向到S,S在自己站點儲存一個session跟cookie,session儲存token物件{tokenID,userName,startTime,endTime},cookie儲存tokenID,tokenID是一個Guid,把token物件快取在集合裡面,另起一個執行緒,根據endTime(過期時間)來定期清理集合列表,重定向到A的時候再將tokenID傳遞過去,拿到tokenID後,進入驗證環節,S站有提供一個介面,根據tokenID獲取token物件,如果獲取到物件,且沒有失效,則tokenID合法,跳入index頁面.情況2,A登入,直接開啟B,這時候B自身沒有session,會主動請求主站,主站會返回cookieID(S站存在客戶端的cookie),這個時候再走驗證環節,如果通過,則B根據token物件建立自身的session,再跳入index.

在這裡整個單點登入就已經成功了,接下來附上流程圖:

前面流程圖,太醜了,這裡補上一張,希望有所幫助.

收集的demo原始碼:

SSO單點登入

說明:新建2個站點xxx-xxx.com(主站),yyy-yyy.com(子站),修改hosts檔案,將這兩個域名都指向127.0.0.1即可.

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

 

相關文章