关闭

青桃传媒

全国

面试官:聊聊单点登录(SSO)

青桃传媒·2025-03-13 16:41:07·阅读

前言

一个规模大点的公司大概率会有很多子系统,每个子系统都是属于公司的,没必要为每个子系统做一个登录系统,因为用户是相通的,把每个子系统的登录部分抽离出来形成一个认证中心,这就是单点登录( Sign On)

实现单点登录的模式比较多, 并没有固定的模式,不过有标准模式(CAS, ),非标准模式,可能每个公司实现方案都不相同。但从技术上来看,大体上可以分为两种,一个 + , 一个 token

单点登录的常见模式 +

面试官:聊聊单点登录(SSO)

用户将账号密码信息发给认证中心,认证中心有个 表格,里面是键值对,key 是生成的全局唯一 id,value 就是用户的身份信息,一旦用户登录成功,表格里面就会记录一条信息。

只要认证中心的 表有这个用户的信息,那么就表明该用户是登录成功的状态,反之, 表没这个信息,用户就会登录失效,有可能是过期了, 表有可能是存在 数据库的,也有可能是存在 redis(内存) 中。

利用 把 sid 带给用户,浏览器就会保存这个 sid,后面浏览器访问子系统时就会把 sid 带过去

子系统并没有这个 表去判定是否有效,于是子系统会将接收到的 sid 发给认证中心,认证中心去查,查到后会告诉子系统该用户完成登录了,具有权限,把身份信息给到子系统

这种模式下有个好处就是认证中心的控制力很强,只要 表删除了用户信息,用户就会立马下线,再配合黑名单,用户就登不上系统了

这种模式下只要用户体量很大,认证中心的压力就会非常大,不同子系统不断的给认证中心发 sid 让他判断,并且表也会非常庞大,还要做 集群,并且认证中心不能挂,你需要给他做一个 容灾;再者,某个子系统的用户体量很大导致该系统要扩容,这样一来,这个子系统给认证中心发 sid 的频率也在变大,随之认证中心也要扩容;这里面所有的缺陷最终都是指向烧钱,为了降低成本,token 模式随之诞生

token

面试官:聊聊单点登录(SSO)

这个模式下的认证中心压力很小

token 模式下用户向认证中心发送登录信息后,认证中心此时并没有向 表去记录任何东西,认证中心会生成一个不能篡改的字符串 token 给用户,这其实就是 jwt ,此前文章有详细讲过/post/…

用户接收到 token 后会将token 存入,可以存 也可以存入 都可以,后面的事情就无关认证中心了。于是用户访问某个子系统时带上 token,子系统是可以自己认证的,具体认证方式比如子系统和认证中心去交换一个密钥,子系统拿到一个密钥之后可以自行认证用户的 token 是否为认证中心颁发的,一旦认证成功就会把受保护的资源发给用户

由此可见,token 模式下认证中心压力就很小了,因为子系统几乎没有向认证中心发送任何的请求,成本随之降低,具体某个子系统用户体量大而去扩容也不会影响到认证中心

缺点也显而易见,认证中心失去了对用户的绝对控制,假设某个用户违规操作,现在希望让这个用户下线,就需要认证中心向每个子系统去发送信息,让用户下线,一两个子系统倒还好,一旦子系统过多就很麻烦

为了解决这个问题双 token 随之诞生

token +

这个模式下有两个 token,一个原 token 一个刷新 token

用户在登录完成后,认证中心会发送两个 token,一个 token 是所有子系统都能识别的,另一个刷新 token 只有认证中心自己认识,原 token 的刷新时间非常短,可能 20min 刷新一次,刷新 token 的过期时间会比较长,比如一周一个月。

假设原 token 没有失效,那么流程就和单token 模式一样的。假设失效了就会如图这样

面试官:聊聊单点登录(SSO)

用户第一次登录会收到认证中心的两个 token,假设用户过了一段时间去登录子系统,原 token 过期了,子系统告诉这个 token 失效,此时用户会将 刷新 token 发给认证中心去验证,认证中心会返回一个新的 token 给到用户,用户再去访问子系统就可以正常访问了

这个模式的意义相较于单 token 模式多了层对用户的控制,比如某个用户违规操作希望让其下线,虽然不能让该用户立即下线,但是原 token 一旦过期,用户拿着 向认证中心索要 token 时,认证中心不管就行,其余子系统是无感的

token 的无感刷新

token 的无感刷新其实主要工作在于后端

看个情形:一般 token 过期时间很短,假设 token 过期时间为 10min,用户登录 10min 后 token 失效就会把你送回登录界面重新登录,此时查看 你会发现其实是携带了 token 的,只不过你的 token 失效了,因此 401 了。每次 10min 后用户都要重新登录下,这样用户体验很糟糕。

如何解决这个问题呢,我们可以加一个刷新 token,也就是 ,这个 token 的过期时间一般会设置较长比如一周、两周、一个月,这个 token 的作用就是去给你替换新的 原token。

所谓 token 无感刷新就是让你 原 token 过期时,前端默默帮你把 替换成了新 token,用户不再需要重新登陆去拿到新的原 token

所以前端想要实现无感刷新的基本思路就是当原 token 过期时,用替换原token,写一个 函数即可,需要去封装 axios,拦截器

这里我也简单在此前 token 文章中的例子基础上是实现下无感刷新

import axios from 'axios'
import router from '../router'
axios.defaults.baseURL = "http://localhost:3000"
// 请求拦截 
axios.interceptors.request.use(config => {
    let token = localStorage.getItem('token')
    if (token) {
        config.headers.Authorization = token
    }
    return config // 把请求拦截下来,并往请求头中加入token,然后return 
})
// 做一个响应拦截,比如登录失败需要提示登录失败 发请求和接受都需要经过axios的手
axios.interceptors.response.use(
    (res) => {
        if (res.data.code && res.data.code !== 0) { // 逻辑性错误,比如密码敲错了,并不是程序性错误
            return Promise.reject(res.data.error) // 这么做的意义是,让axios好去调试,可以捕获错误
        }
        if (res.data.status >= 400 && res.data.status < 500) { // 程序性错误
            // 状态码在[400, 500) 就认为用户没有权限,就强行把你重定向到登录页面
            router.push('/login')
            return Promise.reject(res.data)
        }
        return res  // 响应的内容没有问题
    }
)
export function post(url, body) { 
    return axios.post(url, body)
}

这是那期文章 axios 的封装,这是没有 的情况,现在去增加一个 。也就是 401 时,去刷新 token 再去重新请求

//  刷新 token
const refreashToken = () => {
     await request.get('/refresh_token', {
        headers: {
            Authorization: `${localStorage.getItem(REFRESH_TOKEN_KEY)}`
        }
    })
}
// 做一个响应拦截,比如登录失败需要提示登录失败 发请求和接受都需要经过axios的手
axios.interceptors.response.use(
    (res) => {
        if (res.data.code && res.data.code !== 0) { // 逻辑性错误,比如密码敲错了,并不是程序性错误
            return Promise.reject(res.data.error) // 这么做的意义是,让axios好去调试,可以捕获错误
        }
        if (res.data.status >= 400 && res.data.status < 500) { // 程序性错误
            // 状态码在[400, 500) 就认为用户没有权限,就强行把你重定向到登录页面
            router.push('/login')
            return Promise.reject(res.data)
            if (res.data.status === 401) {
                // 刷新 token
                await refreshToken()
                // 重新请求
                const resp = await axios.request(res.config)
                return resp
            }
        }
        return res  // 响应的内容没有问题
    }
)
export function post(url, body) { 
    return axios.post(url, body)
}

这样写其实有个问题会陷入死循环,res. 里面的 token 还是沿用的失效的 token,因此还需要改下

// 刷新 token
await refreshToken()
// 重新请求
res.config.headers.Authorization = localStorage.getItem('token')
const resp = await axios.request(res.config)
return resp

其实当前写法还是有个问题,当刷新 token 也过期后,依旧会陷入死循环,我们可以在 中加一个条件参数 ,然后判断 401 时加上这个条件即可。另外需要刷新成功才走重新请求,最终写法如下

import axios from 'axios'
import router from '../router'
axios.defaults.baseURL = "http://localhost:3000"
// 请求拦截 
axios.interceptors.request.use(config => {
    let token = localStorage.getItem('token')
    if (token) {
        config.headers.Authorization = token
    }
    return config // 把请求拦截下来,并往请求头中加入token,然后return 
})
//  刷新 token
const refreashToken = () => {
      const resp = await request.get('/refresh_token', {
        headers: {
            Authorization: `${localStorage.getItem(REFRESH_TOKEN_KEY)}`
        },
        _isRefreshToken: true,
    })
    return resp.code === 200
}
const isRefreshRequest = (config) => {
    return !!config._isRefreshToken // 隐式转换为布尔
}
// 做一个响应拦截,比如登录失败需要提示登录失败 发请求和接受都需要经过axios的手
axios.interceptors.response.use(
    (res) => {
        if (res.data.code && res.data.code !== 0) { // 逻辑性错误,比如密码敲错了,并不是程序性错误
            return Promise.reject(res.data.error) // 这么做的意义是,让axios好去调试,可以捕获错误
        }
        if (res.data.status >= 400 && res.data.status < 500) { // 程序性错误
            // 状态码在[400, 500) 就认为用户没有权限,就强行把你重定向到登录页面
            router.push('/login')
            return Promise.reject(res.data)
            if (res.data.status === 401 && !isRefreshToken(res.config)) {
                // 刷新 token
                const isSuccess = await refreshToken()
                if (isSuccess) {
                    // 重新请求
                    res.config.headers.Authorization = localStorage.getItem('token')
                    const resp = await axios.request(res.config)
                    return resp
                }
            }
        }
        return res  // 响应的内容没有问题
    }
)
export function post(url, body) { 
    return axios.post(url, body)
}

当网速比较慢时, 耗时,此时又有其余的请求加入,于是 就会产生多个 造成冗余,此时就可以对 进行优化,如下:

let promise
const refreashToken = () => {
    if (promise) return promise
    promise = new Promise(async (resolve) => {
        const resp = await request.get('/refresh_token', {
            headers: {
                Authorization: `${localStorage.getItem(REFRESH_TOKEN_KEY)}`
            },
            _isRefreshToken: true,
        })
        resolve(resp.code === 200)
    })
    promise.finally(() => {
        promise = null
    })
    return promise
}

协议

Oauth 1.0 版本几乎已经不用了,这里不会概述

协议其实就是你登录第三方网站,这个网址支持你可以通过微信,apple,, 等工具去登录,这样,对于你不信任的网站登录时你不需要提供账号密码,这样的方式就可以避免泄露自己的账号密码等信息

的认证流程如下

面试官:聊聊单点登录(SSO)

假设用户现在通过微信去登录 ,用户只要同意授权,那么认证服务器就会给第三方网站 颁发 token,同意之后, 就可以拿到你的头像等等信息

其实所谓的身份认证,其本质都是基于对请求方的不信任产生的,因此 就是来解决这个问题的

还有个问题就是像是微信这样扮演认证服务器的角色不可能给所有的第三方站点都提供这个 token,因此第三方站点需要向微信申请第三方应用,一般微信,微博,Apple, 都会有自己的 oauth 使用说明

的有几种授权方式,这部分内容以后有空再填

最后

一般来讲,小规模系统 + 就够用了,大规模系统就适合 Token 或者 双Token,若是需要第三方登录就用 。

加载中~

你可能感兴趣的