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

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

作者: 默默无闻的小人物 | 来源:发表于2022-08-15 17:01 被阅读0次

    会话说明:

    因为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>
    
    

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/vniqcrtx.html