前后端分离 SSO单点登录 可用于vue react jsp jq,跨域专用 iframe + postMessage 二级域名主域名 最全SSO单点实战

1,600 阅读5分钟

前言

  1. 最近公司需要做一个SSO单点登录系统,于是上网百度了一些文章,全是一些很模糊的概念,实战起来也很麻烦,这里分享下一个比较简单实用的SSO单点登录方案.

  2. 单点登录 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 跨域通信(注意加上密钥验证)

  1. 接收信息

    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同理...

里面还有一些比较有意思的地方, 感兴趣的同学可在评论区一起探讨

  1. client1登陆的是zs1, client2切换到zs2,client1是怎么切换到zs2的

  2. client1 cookies存储的是zs1,client2切换到zs2, client1 怎么把zs1 的 cookies也切换成zs1的

  3. client1 登录zs1, client2也是zs1,client2重新登录zs1,如何把client1替换最新的token

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

cookies的话二级域名相等可直接拿token,这里不多说了...

旧人以旧/sso-frontend​

gitee.com/frontend-wi…

github.com/frontend-wi…

原创,转载请标注!!!