单点登录主要有三种实现方式:
父域 Cookie
认证中心
LocalStorage 跨域
一般情况下,用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,但是由于不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 SessionId 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在 a.*** 中登录后,Session Id 仅在浏览器访问 a.*** 时才会自动在请求头中携带,而当浏览器访问 b.*** 时,Session Id 是不会被带过去的。实现单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。
1. 父域 Cookie
Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名/IP地址,在 Tomcat 中,domain 属性默认为当前域的域名/IP地址。path 属性的有效值是以“/”开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。
如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,也就是说,子域会自动继承父域中的 Cookie。
利用 Cookie 的这个特点,可以将 Session Id(或 Token)保存到父域中就可以了。我们只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.*** 和 map.baidu.***,它们都建立在 baidu.*** 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。
总结:此种实现方式比较简单,但不支持跨主域名。
2. 认证中心
我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心进行登录。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。
应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(这个 Cookie 是当前应用系统的,其他应用系统是访问不到的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。
总结:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。
3. LocalStorage 跨域
单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。但是 Cookie 是不支持跨主域名的,而且浏览器对 Cookie 的跨域限制越来越严格。
在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session Id (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session Id (或 Token )放在响应体中传递给前端。
在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session Id (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。
总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。
以上内容参考——>:jeep
一般企业级开发sso都是有认证中心的,其余两种方式也能帮助我们更加了解单点登录的实质。
那么接下来就直接上代码了。。。
1.接口封装
首先我们需要拿到获取用户信息,用户登录,用户登出,这些基本的接口,这些接口往往由第三方文档提供,我们只是负责封装,每个接口的具体用法我们后面会说。
2.store状态管理器
store状态管理器是一种用于管理应用程序状态的工具。它是一种中心化的储存系统,用于存放应用程序的状态数据。在应用程序中,各个组件可以通过这个状态管理器来获取和修改应用程序的状态。
所以我们需要在store模块中定义登录的存储信息和getters,action方法
export const userStore = defineStore('user', {
state: () => {
return {
userInfo: null, // 登录信息存储字段-建议每个项目换一个字段,避免与其它项目冲突
userName: null,
a***essToken: null,
jwtToken: null,
menus: null,
robotFlag: false,
robotUrl: null,
}
},
getters: {
getUserInfo(): string {
return this.userInfo
},
getUserName(): string {
return this.userName
},
getA***essToken(): string {
return this.a***essToken
},
getJwtToken(): string {
return this.jwtToken
},
getMenus(): string {
return this.menus
},
getRobotFlag(): boolean {
return this.robotFlag
},
getRobotUrl(): string {
return this.robotUrl
},
},
actions: {
setUserInfo(userInfo: string) {
this.userInfo = userInfo
},
setUserName(userName: string) {
this.userName = userName
},
setA***essToken(a***essToken: string) {
this.a***essToken = a***essToken
},
setMenus(menus: string) {
this.menus = menus
},
setNoJwtToken(jwtToken: string) {
this.jwtToken = jwtToken
},
async setJwtToken(code) {
const tokenRes = await auth.getJwtToken(code)
if (tokenRes === undefined) {
doLogout()
}
let {code: tokenState, data: tokenData} = tokenRes.data
if (constants.RESP_CODE_SUCESS !== tokenState) {
ElNotification.error({
title: '提示',
message: '接口异常'
})
}
let {jwtToken, a***essToken} = tokenData
this.jwtToken = jwtToken
this.a***essToken = a***essToken
const principalRes = await auth.getUser()
let {code: principalState, data: principal} = principalRes.data
this.userInfo = principal
let {username, menus} = principal
this.userName = username
this.menus = menus.filter(menu => {
return (menu.menuType === '2' || menu.menuType === '3') && menu.parent !== config.system.menuRoot
})
},
},
在上述代码中我们观察action中的setJwtToken方法,这里就调用了getJwtToken这个接口,也可以发现我们需要获取一个code来拿到token,那么接下来我们就可以从头开始单点登录了。
3.页面路由拦截
当我们访问一个未登录的页面时,前端需要帮我们跳转到认证中心,去获取code。vue-router提供了路由守卫beforeEach,允许我们在用户进入页面前进行一些操作。
所以现在我们需要开始路由守卫部分的代码分析
首先我们需要引入我们之前写好的store模块,初次之外我们可能还需要一些工具类,和路由信息
store.getJwtToken === null || store.getJwtToken === undefined开始我们就需要判断当前用户是否登录,如果没有登录那么就需要跳转到认证中心进行登录操作,这里我们需要注意跳转到认证中心时我们的url需要拼接些参数这样完成认证时页面才能跳转回来,一般这部分代码是需要文档提供的这里给大家简单看一下代码:
const redirectToSSO = (path, params) => {
const ssoUrl = 'https://sso.example.***/auth'; // SSO服务的URL
const queryParams = {
client_id: 'your_client_id', // 客户端标识
redirect_uri: 'https://yourapp.***/auth/callback', // 回调URL
response_type: 'code', // 响应类型
scope: 'openid profile email', // 授权范围
state: generateRandomState() // 状态参数
// 其他必要的参数...
};
const queryString = new URLSearchParams(queryParams).toString();
const fullUrl = `${ssoUrl}?${queryString}`;
window.location.href = fullUrl;
};
这里我们只需要知道相应类型就是完成认证后会返回code给我们,回调url一般是我们的主页,当我们认证完成时,页面会跳转到主页这时又会进行路由拦截,再次执行store.getJwtToken === null || store.getJwtToken === undefined但此时我们还是没有token,所以我们需要在判断之后,跳转之前增加一个获取token的代码,就是一开始我们在store模块里提及的那个方法setJwtToken,此时的我们还需要接受认证服务返回的code作为这个方法的入参,一般来说code是直接跟随url返回的,所以我们直接从url里截取即可。截取的方法一般我们也会进行封装这里也给大家简单看下
/**
*路由守卫获取code代码
*/
// router history 模式获取code
let code = to.query.code
// router hash模式获取code
code = code || getURLParameter('code')
if (code && code.indexOf('#') !== -1) {
code = code.substring(0, code.indexOf('#'))
}
/**
*路由守卫获取token代码
*/
if (code) {
store.setJwtToken(code) //token会被存储到store中再次使用getJwtToken就会获取到token
}
/**
*从url中获取code工具类
*/
export const getURLParameter = function (key) {
let url = window.location.href
let code = null
url.substring(url.indexOf('?') + 1).split('&').filter(param => {
let pMap = param.split('=')
if (pMap[0] === key) {
code = pMap[1]
}
})
return code
}
这里的router history 模式和router hash模式既可以理解为前者url里不含#后者url里含有#
到这里基本上就结束我们获取到了token代表我们就可以访问前端页面,也可以调用后端接口,获取用户信息。
这篇文章的本意是让我们初步理解下单点登录的流程和一些细节,并不代表按照我的代码就可以实现。对于不同的项目继承第三方往往会有不同的问题,熟悉集成文档有效沟通才是关键。