sso单点登录实现(不同域方案)(主要在前端实现跳转)

会话说明:

因为HTTP协议是一个无状态协议,即Web应用程序无法区分收到的两个HTTP请求是否是同一个浏览器发出的。为了跟踪用户状态,服务器可以向浏览器分配一个唯一ID,并以Cookie的形式发送到浏览器,浏览器在后续访问时总是附带此Cookie,这样,服务器就可以识别用户身份。

Session(以下图例"会话"。说的就是这么回事)
我们把这种基于唯一ID识别用户身份的机制称为Session。每个用户第一次访问服务器后,会自动获得一个Session ID。如果用户在一段时间内没有访问服务器,那么Session会自动失效,下次即使带着上次分配的Session ID访问,服务器也认为这是一个新用户,会分配新的Session ID

请看下图是整个单点登录的流程:
image.png

上图例流程文字说明:

1、用户访问系统A的受保护资源,系统A发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
2、sso认证中心发现用户未登录,将用户引导至登录页面
3、用户输入用户名密码提交登录申请
4、sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
5、sso认证中心带着令牌跳转会最初的请求地址(系统A)
6、系统A拿到令牌,去sso认证中心校验令牌是否有效
7、sso认证中心校验令牌,返回有效,注册系统A
8、系统A使用该令牌创建与用户的会话,称为局部会话,返回受保护资源。
9、用户访问系统B的受保护资源
10、系统B发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
11、 sso认证中心发现用户已登录,跳转回系统B的地址,并附上令牌
12、系统B拿到令牌,去sso认证中心校验令牌是否有效
13、sso认证中心校验令牌,返回有效,注册系统B
14、系统B使用该令牌创建与用户的局部会话,返回受保护资源

先来看看鉴权中心部分的前端实现

项目目录结构如下:

image.png

主要是components下的登录组件以及views里面的index.vue

  • components下的登录组件是做登录操作的
  • views/index.vue 是处理逻辑跳转以及是否显示登录页面。

先看 views/index.vue,这里面用到了组件懒加载

<template>
  <Suspense v-if="showLogin">
    <template #default>
      <login-component />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </Suspense>
</template>
<script>
import { defineAsyncComponent, defineComponent, ref } from 'vue'
import { getUserInfo, clearUserInfo } from 'lib@/utils/sso-user'
import { getUrlQueryString } from 'lib@/utils/utils'

export default defineComponent({
  name: 'sso-login',
  components: {
    loginComponent: defineAsyncComponent(() =>
      import('./../components/login/index')
    ),
  },
  setup() {
    let showLogin = ref(false)
    const clearUser = getUrlQueryString('clearUser')
    let service = getUrlQueryString('service')
    let redirectUrl = service
    if(service&&service.lastIndexOf('/')===service.length-1){
      redirectUrl= service.substring(0,service.length)
    }
    function isAutoLogin() {
      const userInfo = getUserInfo()
      console.log(redirectUrl)
      const token = userInfo ? userInfo.token : ''
      // 校验是否已经登录过,如果登录了就带上有效token跳转回调地址
      if (!!token || token === 0) {
        // 判断是否有回调地址,如果有则直接重定向,如果没有就显示接入了单点登录的系统列表
        if (redirectUrl) {
          window.location.href = `${redirectUrl}?token=${token}`
        } else {
          window.location.href=`${window.location.protocol}//${window.location.host}/join-sso-list.html`
        }
      } else {
        // 否则显示登录
        showLogin.value = true
      }
    }
    if (clearUser) {
      clearUserInfo()
    }
    isAutoLogin()
    return {
      showLogin,
    }
  },
})
</script>

主要逻辑是,当访问这个鉴权系统的时候会先判断是否有登录,有登录则直接取当前系统的令牌然后跳转回回调系统。如果没有登录则会显示登录页面。然后进行常规登录之后再带上令牌跳转回回调系统。

再看登录逻辑组件

<template>
  <div class="login-layout">
    <div class="login-main">
      <header class="login-header">
        <img src="../../../../assets/images/logo.png" class="logo" />
        <span class="login-header__text">统一单点登陆</span>
      </header>
      <div class="login-header__desc"></div>
      <main class="login-content">
        <el-form ref="formRef" :model="form" :rules="rules">
          <el-form-item prop="name">
            <el-input
              v-model="form.name"
              placeholder="用户名"
              prefix-icon="i-user"
              clearable
              autofocus
            ></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input
              v-model="form.password"
              type="password"
              prefix-icon="i-unlock"
              placeholder="密码"
              clearable
              @keyup.enter="onSubmit"
            ></el-input>
          </el-form-item>
          <!-- <el-form-item>
            <a href="/register.html" style="float: left">注册账号</a>
          </el-form-item> -->
          <el-form-item>
            <el-button type="primary" class="login-btn" @click="onSubmit"
              >登录</el-button
            >
          </el-form-item>
        </el-form>
        <a href="/register.html" style="float: left">注册账号</a>
      </main>
      <footer>
        <span class="login-msg">{{ msg }}</span>
      </footer>
    </div>
  </div>
</template>

<script>
import { defineComponent, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import http from 'api@/index'
import md5 from 'md5'
import { msgWarning } from 'lib@/utils/el-utils'
import { SYSTEM_CODE_ENUM } from '@/config/enums/system.js'
import { getUrlQueryString } from 'lib@/utils/utils'
import { setUserInfo, clearUserInfo } from 'lib@/utils/sso-user'

export default defineComponent({
  name: 'login-component',
  setup() {
    let form = reactive({
      name: '',
      password: ''
    })
    let formRef = ref(null)
    let rules = reactive({
      name: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
      password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
    })
    let msg = ref(null)
    const redirectUrl = getUrlQueryString('service')
    async function onSubmit() {
      msg.value = null
      await formRef.value.validate((valid) => {
        if (valid) {
          http
            .post('authorityLogin', {
              userAccount: form.name,
              pwd: md5(form.password)
            })
            .then((res) => {
              if (res.code === SYSTEM_CODE_ENUM.SUCCESS) {
                /* 登录后清除信息  并设置新的用户信息 */
                clearUserInfo()
                setUserInfo(res.data)
                // 判断是否有回调地址,如果有则带着token重定向,如果没有就显示接入了单点登录的系统列表
                if (redirectUrl) {
                  window.location.href = `${redirectUrl}?token=${res.data.token}`
                } else {
                  window.location.href=`${window.location.protocol}//${window.location.host}/join-sso-list.html`
                }
              } else {
                msg.value = res.message
              }
            })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }

    return {
      form,
      onSubmit,
      formRef,
      rules,
      msg
    }
  }
})
</script>

鉴权中心系统代码就这么多了


下面看接入系统这么实现

思路就是在访问到我们系统的时候先执行一段登录校验逻辑,你可以单独抽成一个js文件,然后在index.html中调用。方式多样。反正就是系统初始化的时候先执行登录校验逻辑。判断是否登录,跳转到鉴权中心等
下面贴一下代码,因为我是用的vue3,所以我将逻辑封装成 compostion-api 方式,也是一个单独的js。在app.vue中注入。或者main.js中调用就行。

import {
  setUserInfo,
  clearUserInfo,
  setMenuInfo,
  getMenuInfo,
  setUserOrgInfo,
  setUserOrgList,
  clearUserOrg,
  isUserLogin
} from 'lib@/utils/user'
import http from 'api@/index'
import { getUrlQueryString, navSSOLogout } from 'lib@/utils/utils'
import { SYSTEM_CODE_ENUM } from '@/config/enums/system.js'
import { useRouter } from 'vue-router'

export default function isLoginControl() {
  const routeToken = getUrlQueryString('token')
  const router = useRouter()
  return new Promise((resolve, reject) => {
    // 获取路由参数里面的token
    // 如果url地址有token则是中鉴权中心回调回来的,需要调用校验toekn有效的接口·
    if (routeToken) {
      // 调用鉴权中心接口去校验
      http
        .post('authVerifyToken', {
          token: routeToken
        })
        .then(r => {
          if (r.code === SYSTEM_CODE_ENUM.SUCCESS && r.data) {
            console.log('4.0验证接口返回验证成功')
            let { token, userAccount, userName, sessionId, apiAuths, itemAuths,activeOrgs, currentOrg, currentOrgName } = r.data
            if(itemAuths.length === 0){
              router.replace({path:'/noAuth'})
              resolve('校验成功')
              return
            }
            setUserInfo({ token, userAccount, userName, sessionId })
            setMenuInfo(itemAuths)
            // 设置组织信息
            if (activeOrgs && activeOrgs.length > 0) {
              setUserOrgList(activeOrgs)
              if (currentOrg && currentOrgName) {
                setUserOrgInfo({ orgCode: currentOrg, orgName: currentOrgName })
              } else {
                setUserOrgInfo({ orgCode: activeOrgs[0].orgCode, orgName: activeOrgs[0].orgName })
              }
            } else {
              clearUserOrg()
            }
            // 去掉url中的token参数
            router.replace({ path: window.location.pathname })
            resolve('校验成功')
          } else {
            console.log('4.0验证接口返回验证失败')
            clearUserInfo()
            // 验证失败,token无用,需要跳转到sso登录页,重新进行登录
            navSSOLogout(true)
            reject('校验失败')
          }
        })
        .catch(err => {
          reject(err)
        })
    } else {
      console.log('4.0获取session的token')
      // 判断是否登录
      if (!isUserLogin()) {
        console.log('4.0session没有跳转登录页')
        // 携带跳转到sso-login页面
        navSSOLogout()
        reject('未登录')
      } else {
        const menuInfo = getMenuInfo()
        if((!!menuInfo===false)||menuInfo.length===0){
          router.replace({path:'/noAuth'})
          resolve('校验成功')
          return
        }
        resolve('已经登录')
      }
    }
  })
}

app.vue中调用导出的 isLoginControl()方法

<template>
  <el-config-provider v-if="show" :locale="locale">
    <router-view />
  </el-config-provider>
</template>
<script>
import { defineComponent, ref, onBeforeMount } from 'vue'
import { ElConfigProvider } from 'element-plus'
import i18n from 'lib@/utils/i18n/index'
import isLoginControl from 'lib@/compostion-api/is-login-control'
export default defineComponent({
  components: {
    ElConfigProvider,
  },
  setup() {
    const locale = i18n.global.locale
    let show = ref(false)
    isLoginControl()
      .then((r) => {
        show.value = true
      })
      .catch((err) => {
        console.log(err)
      })
    return {
      locale: i18n.global.messages[locale],
      show,
    }
  },
})
</script>


<style lang="less">
@import "~assets@/styles/main.less";
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  height: 100vh;
}
body,
html {
  padding: 0;
  margin: 0;
}
</style>

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

推荐阅读更多精彩内容