会话说明:
因为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使用该令牌创建与用户的局部会话,返回受保护资源
先来看看鉴权中心部分的前端实现
项目目录结构如下:
主要是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>
网友评论