前后端分离 SSO单点登录 可用于vue react jsp jq,跨域专用 iframe + postMessage 二级域名主域名 最全SSO单点实战
前言
最近公司需要做一个SSO单点登录系统,于是上网百度了一些文章,全是一些很模糊的概念,实战起来也很麻烦,这里分享下一个比较简单实用的SSO单点登录方案.
单点登录 SSO 全称 Singn Sign On 。SSO 是指在多个应用系统中,用户只需要登录一次用户系统,就可以访问其他互相信任的应用系统。例如:在淘宝登录账户,那么再进入天猫等其他业务的时候会自动登录。另外还有一个好处就是在一定时间内可以不用重复登录账户。
废话不多说直接上视频,看看是不是想要的效果
四个参数 loginname password type info
上传视频封面
client1 初始化 token loginname type info都为空,点击登录创建token(实际项目是有个SSO登录系统的,点击登录的时候把token存储到localStorage即可),这个时候来到clinet2 refresh 刷新token已经传递过来了,在client1退出的时候,回到client2 refresh也是退出状态.达到了SSO登录 client1登录,client1也是登录状态,client1 退出,client2也是退出状态
到这里有的小伙伴就问了那我什么时候执行 refresh 事件呢? 答案是: 页面请求的每一个接口相应拦截(比如vue项目就使用axios统一拦截器)
那么是怎么实现跨域能让SSO登录了 client1, client2 都能拿到SSO的token呢?
使用iframe + postMessage 跨域通信(注意加上密钥验证)
接收信息
const receiveMsg = function(event) {const user = event.data
if (user.token) {
}}window.addEventListener('message', receiveMsg, false)
2. 发送信息
const monitor = document.getElementById(id)monitor.contentWindow.postMessage( { user }, // sso地址 html sso.html)
3. 代码说明
sso做的事情: 获取本地token发送全局信息出去
client做的事情: 发送指定信息(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' }, '*' ) } // 监听事件 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 退出登录状态 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 退出登录状态 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状态 isSuccess: false } /**
* @description: 防抖函数
* @param {*} fn 函数
* @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: 创建公共网页
* @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所占用的内存。
* @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秒之内没有获取到data提示用户获取信息失败 // 场景:断网,程序出错,服务挂了 setTimeout(() => { if (!this.state.isSuccess) { window.confirm('获取用户信息失败,请联系管理员或者重新获取。') window.location.reload() } }, 5000); } /**
* @description: 全局发送信息
* @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('登录状态以失效,退出登录页面') // 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}