前言
最近在做單點(diǎn)登錄的前端適配工作,包括移動(dòng)端、小程序、web。也許一些人還沒有接觸過單點(diǎn)登錄,接下來我將以keycloak為例進(jìn)行探討。首先介紹兩個(gè)概念:
單點(diǎn)登錄(Single Sign On),簡稱為 SSO,是比較流行的企業(yè)業(yè)務(wù)整合的解決方案之一。SSO的定義是在多個(gè)應(yīng)用系統(tǒng)中,用戶只需要登錄一次就可以訪問所有相互信任的應(yīng)用系統(tǒng)。(百度百科)
Keycloak是一個(gè)為瀏覽器和RESTful Web服務(wù)提供 SSO 的集成。
聲明:本文僅代表個(gè)人觀點(diǎn)
Refresh_token
如果做過微信開發(fā)的人應(yīng)該看到過refresh_token這個(gè)概念。我就直接引用官方的解釋
refresh_token由于access_token擁有較短的有效期,當(dāng)access_token超時(shí)后,可以使用refresh_token進(jìn)行刷新,refresh_token有效期為30天,當(dāng)refresh_token失效之后,需要用戶重新授權(quán)。(微信開放文檔)
簡單介紹一下,在token即將過期時(shí),通過refresh_token來拿到最新的token
在接下來的介紹中也會(huì)用到refresh這個(gè)概念。
流程圖
先來看一下整體的流程圖,如果不是很明白,可以先往下看,之后再翻上來看就容易理解了
keycloak單點(diǎn)登錄前端適配流程圖
先來想幾個(gè)問題
- 首次登錄如何處理?
- 之前登錄過如何處理?(考慮關(guān)掉后和重新打開的間隔時(shí)間)
- 用戶在使用當(dāng)中,網(wǎng)絡(luò)斷了,然后幾分鐘網(wǎng)好了如何處理?(比如過隧道)
- 定時(shí)器選擇setInterval還是選擇setTimeout?(考慮使用過程中,token過期的時(shí)間是否可能在服務(wù)端被修改)
問題1和問題2我們可以在流程圖中找到答案;
問題3(考慮用戶體驗(yàn)):在過隧道的時(shí)候,假如正好是定時(shí)器請(qǐng)求刷新token的時(shí)間,這時(shí)候刷新token肯定會(huì)失敗,那也不能直接就讓定時(shí)器斷掉,而是多次嘗試重新連接,直到refresh_token也過期。
問題4:考慮到token過期的時(shí)間會(huì)被在服務(wù)端更改,所以setTimeout是更好的選擇
核心代碼
token檢查,我們需要考慮超時(shí)時(shí)間(我們的技術(shù)棧是vue)
// 返回值為過期時(shí)間
function checkTokenExpire(type: string) {
let kcData: any = localStorage.getItem('keycloak')
if (kcData) kcData = JSON.parse(kcData)
if (!kcData || !kcData[type]) return -1
const TokenParsed = kcData[type]
if (!TokenParsed.exp) return -1
const exp = TokenParsed.exp * 1000 - 15000
return exp - now()
}
// token過期的時(shí)間
export function tokenNotExpire() {
return checkTokenExpire('tokenParsed')
}
// refresh_token過期的時(shí)間
export function refreshTokenNotExpire() {
return checkTokenExpire('refreshTokenParsed')
}
刷新token,這里會(huì)考慮到“過隧道”的情況
export async function refreshToken() {
try {
const refreshed = await store.state.keycloak.updateToken(-1)
if (refreshed) {
const kc = await initKc()
localStorage.setItem('keycloak', JSON.stringify(kc))
const exp = kc.tokenParsed?.exp
if (exp) return exp * 1000 - 15000 - now()
}
} catch (error) {
if (refreshTokenNotExpire() > 0) {
setInteralToken(5000, refreshToken)
}
}
}
檢查token過期檢查策略
export async function tokenCheckStrategy() {
// t, rt為有效期(毫秒)
const t = tokenNotExpire()
if (t <= 0) {
const rt = refreshTokenNotExpire()
if (rt <= 0) return false
// 如果refresh_token沒有過期,通過refresh_token拿到access_token, 并且開啟刷新token定時(shí)器
const tokenExp = await refreshToken()
if (tokenExp) {
setInteralToken(tokenExp, refreshToken)
return true
}
return false
}
setInteralToken(t, refreshToken)
return true
}
定時(shí)器需要在得到有效數(shù)據(jù)才能繼續(xù)
export function setInteralToken(exp: number, refreshTokenFn: Function) {
if (exp > 0) {
setTimeout(async () => {
const t = await refreshTokenFn()
setInteralToken(t, refreshTokenFn)
}, exp)
}
}
