2021-07-11

前后端分離 SSO單點登錄 可用于vue react jsp jq,跨域?qū)S?iframe + postMessage 二級域名主域名 最全SSO單點實戰(zhàn)


前言

最近公司需要做一個SSO單點登錄系統(tǒng),于是上網(wǎng)百度了一些文章,全是一些很模糊的概念,實戰(zhàn)起來也很麻煩,這里分享下一個比較簡單實用的SSO單點登錄方案.

單點登錄 SSO 全稱 Singn Sign On 。SSO 是指在多個應用系統(tǒng)中榨汤,用戶只需要登錄一次用戶系統(tǒng)咬展,就可以訪問其他互相信任的應用系統(tǒng)涌萤。例如:在淘寶登錄賬戶,那么再進入天貓等其他業(yè)務的時候會自動登錄唤锉。另外還有一個好處就是在一定時間內(nèi)可以不用重復登錄賬戶草描。

廢話不多說直接上視頻,看看是不是想要的效果

四個參數(shù) loginname password type info

上傳視頻封面

client1 初始化 token loginname type info都為空,點擊登錄創(chuàng)建token(實際項目是有個SSO登錄系統(tǒng)的,點擊登錄的時候把token存儲到localStorage即可),這個時候來到clinet2 refresh 刷新token已經(jīng)傳遞過來了,在client1退出的時候,回到client2 refresh也是退出狀態(tài).達到了SSO登錄 client1登錄,client1也是登錄狀態(tài),client1 退出,client2也是退出狀態(tài)

到這里有的小伙伴就問了那我什么時候執(zhí)行 refresh 事件呢? 答案是: 頁面請求的每一個接口相應攔截(比如vue項目就使用axios統(tǒng)一攔截器)

那么是怎么實現(xiàn)跨域能讓SSO登錄了 client1, client2 都能拿到SSO的token呢?

使用iframe + postMessage 跨域通信(注意加上密鑰驗證)

接收信息

const receiveMsg = function(event) {const user = event.data

? if (user.token) {? ?

? }}window.addEventListener('message', receiveMsg, false)

2. 發(fā)送信息

const monitor = document.getElementById(id)monitor.contentWindow.postMessage( { user }, // sso地址 html sso.html)

3. 代碼說明

sso做的事情: 獲取本地token發(fā)送全局信息出去

client做的事情: 發(fā)送指定信息(get, undata),通過接收window.addEventListener('message', fun..., false)接收信息 do someing ...

4. 代碼展示

5. sso.html

'use strict'class Sso {? state = {? ? // 密鑰? ? secretKey: 'SSO-DATA',? ? // remove id? ? removeId: 'remove',? }? init = () => {? ? document.getElementById('sso').innerText = 'SSO data sharing center'? ? window.addEventListener('message', (e) => this.receiveMsg(e), false)? ? // 初始化? ? window.parent.postMessage(? ? ? { init: 'init' },? ? ? '*'? ? )? }? // 監(jiān)聽事件? receiveMsg = (e) => {? ? const data = e.data

? ? if (data) {? ? ? // 退出標識符? ? ? const removeId = this.state.removeId

? ? ? const user = data.user

? ? ? if (user) {? ? ? ? const { secretKey } = user

? ? ? ? if (!secretKey) {? ? ? ? ? throw '密鑰不能為空!'? ? ? ? } else if (window.atob(secretKey) !== this.state.secretKey) {? ? ? ? ? throw '密鑰錯誤!'? ? ? ? }? ? ? ? if (user.type && user.type === 'updata') {? ? ? ? ? // 更新user? ? ? ? ? const { loginname, token, password } = user

? ? ? ? ? localStorage.setItem(? ? ? ? ? ? 'user',? ? ? ? ? ? JSON.stringify({? ? ? ? ? ? ? loginname,? ? ? ? ? ? ? token,? ? ? ? ? ? ? password,? ? ? ? ? ? })? ? ? ? ? )? ? ? ? ? this.setCookie('loginname', loginname, 1)? ? ? ? ? this.setCookie('token', token, 1)? ? ? ? ? window.parent.postMessage({ loginname, token, password }, '*')? ? ? ? } else {? ? ? ? ? // 查找本地 user? ? ? ? ? const localUser = localStorage.getItem('user')? ? ? ? ? // 查找本地 cookies? ? ? ? ? const userCookie = this.getCookie('token')? ? ? ? ? if (localUser) {? ? ? ? ? ? const { loginname, token, password } = JSON.parse(localUser)? ? ? ? ? ? if (? ? ? ? ? ? ? (token && token !== removeId) ||? ? ? ? ? ? ? (password && password !== removeId)? ? ? ? ? ? ) {? ? ? ? ? ? ? if (userCookie && userCookie === removeId) {? ? ? ? ? ? ? ? // cookies 退出登錄狀態(tài)? ? ? ? ? ? ? ? window.parent.postMessage(? ? ? ? ? ? ? ? ? { token: removeId },? ? ? ? ? ? ? ? ? '*'? ? ? ? ? ? ? ? )? ? ? ? ? ? ? } else if (userCookie && userCookie !== removeId && userCookie !== 'undefined' && userCookie !== 'null' && userCookie !== token) {? ? ? ? ? ? ? ? // cookies 和 local的token不一致? ? ? ? ? ? ? ? const newUser = {? ? ? ? ? ? ? ? ? loginname: this.getCookie('loginname'),? ? ? ? ? ? ? ? ? token: userCookie,? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? localStorage.setItem('user', JSON.stringify(newUser))? ? ? ? ? ? ? ? window.parent.postMessage(newUser, '*')? ? ? ? ? ? ? } else {? ? ? ? ? ? ? ? // 正常放行? ? ? ? ? ? ? ? window.parent.postMessage({ loginname, token, password }, '*')? ? ? ? ? ? ? }? ? ? ? ? ? } else if (userCookie && userCookie !== removeId) {? ? ? ? ? ? ? // 如果cookies有token? ? ? ? ? ? ? if (token === removeId) {? ? ? ? ? ? ? ? // local 退出登錄狀態(tài)? ? ? ? ? ? ? ? window.parent.postMessage(? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? token: removeId

? ? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? ? '*'? ? ? ? ? ? ? ? )? ? ? ? ? ? ? } else {? ? ? ? ? ? ? ? const userObj = {? ? ? ? ? ? ? ? ? loginname: this.getCookie('user'),? ? ? ? ? ? ? ? ? token: userCookie

? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? window.parent.postMessage(userObj, '*')? ? ? ? ? ? ? }? ? ? ? ? ? } else {? ? ? ? ? ? ? window.parent.postMessage(? ? ? ? ? ? ? ? { loginname, token, password },? ? ? ? ? ? ? ? '*'? ? ? ? ? ? ? )? ? ? ? ? ? }? ? ? ? ? } else {? ? ? ? ? ? window.parent.postMessage(? ? ? ? ? ? ? { token: removeId },? ? ? ? ? ? ? '*'? ? ? ? ? ? )? ? ? ? ? }? ? ? ? }? ? ? } else {? ? ? ? window.parent.postMessage(? ? ? ? ? { data },? ? ? ? ? '*'? ? ? ? )? ? ? }? ? }? }? // 存儲二級域名? setCookie = (cname, cvalue, exdays) => {? ? const d = new Date()? ? d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)? ? const expires = 'expires=' + d.toGMTString()? ? let hostArr = window.location.hostname.split('.')? ? // 注意 cookies 只有 '同級' 域名 才能共享 (這里只取最后兩位)? ? let cdomain = hostArr.slice(-2).join('.')? ? const domain = 'domain=' + cdomain

? ? document.cookie = `${cname}=${cvalue};${expires};${domain};path=/`? }? getCookie = (cname) => {? ? const name = cname + '='? ? const ca = document.cookie.split(';')? ? for (let i = 0; i < ca.length; i++) {? ? ? const c = ca[i].trim()? ? ? if (c.indexOf(name) == 0) {? ? ? ? return c.substring(name.length, c.length)? ? ? }? ? }? ? return ''? }? checkCookie = (cname, cvalue, exdays) => {? ? this.setCookie(cname, cvalue, exdays)? }}window.onload = function () {? new Sso().init()}

6. client.html

class SsoClient {? state = {? ? // iframe url? ? mainOrigin: 'http://192.168.0.100:5000',? ? // iframe id (唯一)? ? iframeId: 'monitor',? ? // need init? ? isInit: true,? ? // remove id? ? removeId: 'remove',? ? // base64 密鑰? ? secretKey: 'U1NPLURBVEE=',? ? // 建立iframe狀態(tài)? ? isSuccess: false? }? /**

? * @description: 防抖函數(shù)

? * @param {*} fn 函數(shù)

? * @param {*} delay 毫秒

? */? _debounce(fn, delay = 200) {? ? let timer

? ? return function () {? ? ? const that = this? ? ? let args = arguments

? ? ? if (timer) {? ? ? ? clearTimeout(timer)? ? ? }? ? ? timer = setTimeout(function () {? ? ? ? timer = null? ? ? ? fn.apply(that, args)? ? ? }, delay)? ? }? }? /**

? * @description: 創(chuàng)建公共網(wǎng)頁

? * @description: 注意:id有默認值 建議還是傳一個值 要不然有各種莫名奇怪的問題

? * @param { id } 唯一id

? * @return Promise

? */? appendIframe(id = this.state.iframeId) {? ? return new Promise(async resolve => {? ? ? const iframe = document.getElementById(id)? ? ? if (!iframe) {? ? ? ? // await this.destroyIframe()? ? ? ? const ssoSrc = this.state.mainOrigin + '/sso/'? ? ? ? const i = document.createElement('iframe')? ? ? ? i.style.display = 'none'? ? ? ? i.src = ssoSrc

? ? ? ? i.id = id

? ? ? ? document.body.appendChild(i)? ? ? ? resolve('')? ? ? }? ? })? }? /**

? * @description: 銷毀iframe衔掸,釋放iframe所占用的內(nèi)存脊僚。

? * @description: 注意:id有默認值 建議還是傳一個值 要不然有各種莫名奇怪的問題

? * @param { id } 唯一id

? * @return Promise

? */? destroyIframe(id = this.state.iframeId) {? ? return new Promise(resolve => {? ? ? const iframe = document.getElementById(id)? ? ? if (iframe) {? ? ? ? iframe.parentNode.removeChild(iframe)? ? ? ? resolve('')? ? ? }? ? })? }? /**

? * @description: 建立 iframe 連接

? * @description: 初始化會自動注冊

? */? initMiddle = async () => {? ? await this.appendIframe()? ? window.addEventListener('message', this.getMiddleInfo, false)? ? // 5秒之內(nèi)沒有獲取到data提示用戶獲取信息失敗? ? // 場景:斷網(wǎng),程序出錯饿敲,服務掛了? ? setTimeout(() => {? ? ? if (!this.state.isSuccess) {? ? ? ? window.confirm('獲取用戶信息失敗冈爹,請聯(lián)系管理員或者重新獲取割岛。')? ? ? ? window.location.reload()? ? ? }? ? }, 5000);? }? /**

? * @description: 全局發(fā)送信息

? * @param: {get} 查詢

? * @param: {updata -> } 場景1: 退出登錄

? */? postMiddleMessage = (type = 'get', user = { type: 'get' }) => {? ? // iframe實例? ? const contentWindow = document.getElementById(this.state.iframeId).contentWindow

? ? // 密鑰 (必傳)? ? user.secretKey = this.state.secretKey

? ? if (type === 'updata' && JSON.stringify(user) !== '{}') {? ? ? contentWindow.postMessage({? ? ? ? user

? ? ? }, this.state.mainOrigin)? ? } else {? ? ? // 默認查詢? ? ? contentWindow.postMessage({? ? ? ? user

? ? ? }, this.state.mainOrigin)? ? }? }? /**

? * @description: 實時處理iframe信息

? * @param {*} event

? */? getMiddleInfo = (event) => {? ? if (this.state.isInit) {? ? ? // 初始化? ? ? this.postMiddleMessage('get')? ? }? ? if (event.origin === this.state.mainOrigin) {? ? ? // 建立 成功? ? ? this.state.isInit = false? ? ? this.state.isSuccess = true? ? ? const data = event.data

? ? ? this.businss(data)? ? }? }? /**

? * @description: do someing

? * @description: 全局處理iframe信息

? * @param {data} {token, loginname, password, type}

? */? businss = (data) => {? ? // console.log(data , 'success');? ? if (data.token || data.password) {? ? ? // 獲取信息? ? ? if (data.token === this.state.removeId) {? ? ? ? this.rmLocal()? ? ? ? // alert('登錄狀態(tài)以失效,退出登錄頁面')? ? ? ? // window.location.reload()? ? ? } else {? ? ? ? // 初始化獲取信息成功? ? ? ? if (data.token) {? ? ? ? ? this.setLocal('user', data)? ? ? ? } else {? ? ? ? ? console.log('password');? ? ? ? }? ? ? }? ? ? document.getElementById('content').innerHTML = `? ? <h3>loginname:${data.loginname}</h3>

? ? <h3>token:${data.token}</h3>

? ? <h1>password:${data.password}</h1>

? ? <h3>info:${data.info}</h3>

? ? `? ? } else {? ? }? }? getLocal = (key = 'user') => {? ? return JSON.parse(sessionStorage.getItem(key))? }? setLocal = (key = 'user', data) => {? ? return sessionStorage.setItem(key, JSON.stringify(data))? }? rmLocal = (key = 'user') => {? ? return sessionStorage.removeItem(key)? }}window.onload = function () {? const initSsoClient = new SsoClient()? initSsoClient.initMiddle()? window.initSsoClient = initSsoClient}

如果在vue,react里面使用的話,需要全局攔截(router.beforeEach),iframe收到sso發(fā)送的token信息再next(),react同理...

里面還有一些比較有意思的地方, 感興趣的同學可在評論區(qū)一起探討

client1登陸的是zs1, client2切換到zs2,client1是怎么切換到zs2的

client1 cookies存儲的是zs1,client2切換到zs2, client1 怎么把zs1 的 cookies也切換成zs1的

client1 登錄zs1, client2也是zs1,client2重新登錄zs1,如何把client1替換最新的token

二級域名下可共享cookies(cookies有很多限制,首先拿local,再是cookies)

cookies的話二級域名相等可直接拿token,這里不多說了...

https://gitee.com/frontend-winter/sso-frontend

https://gitee.com/frontend-winter/sso-frontend (github.com)github.com

原創(chuàng),轉(zhuǎn)載請標注!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市犯助,隨后出現(xiàn)的幾起案子癣漆,更是在濱河造成了極大的恐慌,老刑警劉巖剂买,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惠爽,死亡現(xiàn)場離奇詭異,居然都是意外死亡瞬哼,警方通過查閱死者的電腦和手機婚肆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坐慰,“玉大人较性,你說我怎么就攤上這事〗嵴停” “怎么了赞咙?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長糟港。 經(jīng)常有香客問我攀操,道長,這世上最難降的妖魔是什么秸抚? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任速和,我火速辦了婚禮,結(jié)果婚禮上剥汤,老公的妹妹穿的比我還像新娘颠放。我一直安慰自己,他們只是感情好吭敢,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布碰凶。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪痒留。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天蠢沿,我揣著相機與錄音伸头,去河邊找鬼。 笑死舷蟀,一個胖子當著我的面吹牛恤磷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播野宜,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扫步,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匈子?” 一聲冷哼從身側(cè)響起河胎,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虎敦,沒想到半個月后游岳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡其徙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年胚迫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唾那。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡访锻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闹获,到底是詐尸還是另有隱情期犬,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布避诽,位于F島的核電站哭懈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏茎用。R本人自食惡果不足惜遣总,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望轨功。 院中可真熱鬧旭斥,春花似錦、人聲如沸古涧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菇爪,卻和暖如春算芯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凳宙。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工熙揍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人氏涩。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓届囚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親是尖。 傳聞我的和親對象是個殘疾皇子意系,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 本周六結(jié)束了線下課程,轉(zhuǎn)為線上饺汹,更加努力蛔添,fighting! 今天記錄一下我在學習scrapy爬取知乎用戶詳細信息...
    大竹英雄閱讀 165評論 0 0
  • 先來看看實現(xiàn)效果吧 前端原理 在 a.com 登錄之后我們要實現(xiàn) b.com 打開后自動登錄,我們知道兩者之間必不...
    Catlina1996閱讀 1,754評論 0 1
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理兜辞,服務發(fā)現(xiàn)作郭,斷路器,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • 還需要完善的點 TCP/IP五層模型的協(xié)議 OSI七層模 -> 應用層下面有表示層和會話層 應用層 // http...
    執(zhí)涼閱讀 337評論 0 0
  • 推薦閱讀:前端常見跨域解決方案 寫的很全面很詳細 https://segmentfault.com/a/11900...
    Jc_wo閱讀 1,252評論 0 0