前后端分離 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}