2021-07-11

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

如果在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,这里不多说了...

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

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

原创,转载请标注!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 本周六结束了线下课程,转为线上,更加努力,fighting! 今天记录一下我在学习scrapy爬取知乎用户详细信息...
    大竹英雄阅读 165评论 0 0
  • 先来看看实现效果吧 前端原理 在 a.com 登录之后我们要实现 b.com 打开后自动登录,我们知道两者之间必不...
    Catlina1996阅读 1,756评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • 还需要完善的点 TCP/IP五层模型的协议 OSI七层模 -> 应用层下面有表示层和会话层 应用层 // http...
    执凉阅读 339评论 0 0
  • 推荐阅读:前端常见跨域解决方案 写的很全面很详细 https://segmentfault.com/a/11900...
    Jc_wo阅读 1,254评论 0 0