前言
废话不多说直接上视频,看看是不是想要的效果
四个参数 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
}
如果在vue,react里面使用的话,需要全局拦截(router.beforeEach),iframe收到sso发送的token信息再next(),react同理...
里面还有一些比较有意思的地方, 感兴趣的同学可在评论区一起探讨
-
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,这里不多说了...
旧人以旧/sso-frontend
原创,转载请标注!!!