token 機(jī)制和實(shí)現(xiàn)方式
前言
之前在面試的時(shí)候被問(wèn)到過(guò)刷新 token 的問(wèn)題,其實(shí)我對(duì) token 驗(yàn)證機(jī)制的細(xì)節(jié)一直不清楚。新項(xiàng)目和后端的同學(xué)商量后使用刷新 token 來(lái)實(shí)現(xiàn)。本文主要分享一下對(duì) token 機(jī)制的理解和實(shí)現(xiàn)方式。
登錄驗(yàn)證的方式
登錄驗(yàn)證一般來(lái)說(shuō)有兩個(gè)目的,一個(gè)是為了安全,一個(gè)是為了用戶方便。因?yàn)?HTTP 是無(wú)狀態(tài)的,所以后端在接受到請(qǐng)求之后并不能知道請(qǐng)求是從哪里來(lái)的,但是很多時(shí)候我們有驗(yàn)證用戶身份的需求,同時(shí)前端又有保存用戶登錄狀態(tài)的需求。而如果將用戶信息保存在前端,必然是非常危險(xiǎn)的,很容易被獲取,所以就有了在后端進(jìn)行非對(duì)稱加密的方式來(lái)實(shí)現(xiàn)登錄的驗(yàn)證和保存。
目前主要的登錄驗(yàn)證方式有 cookie + session,token,單點(diǎn)登錄和 OAuth 第三方登錄。本文我們主要講一講 token 登錄驗(yàn)證。
什么是 token
token 直譯就是令牌的意思,其實(shí)就是后端將用戶信息進(jìn)行非對(duì)稱加密,然后將加密后的內(nèi)容保存在前端,當(dāng)發(fā)送請(qǐng)求的時(shí)候帶上這個(gè)令牌來(lái)實(shí)現(xiàn)身份驗(yàn)證。大致的過(guò)程是第一次登錄用戶輸入用戶名和密碼,服務(wù)器驗(yàn)證無(wú)誤后會(huì)對(duì)用戶的信息進(jìn)行非對(duì)稱加密生成一個(gè)令牌返回給前端,前端可以存入 cookie 或者 localStorage 等,以后每次發(fā)送請(qǐng)求帶上這個(gè)令牌,后端通過(guò)對(duì)令牌的驗(yàn)證來(lái)識(shí)別用戶的身份以及請(qǐng)求的合法性。
token 的優(yōu)點(diǎn)是服務(wù)端不需要保存 token,只需要驗(yàn)證前端傳過(guò)來(lái)的 token 即可,所以幾遍是分布式部署也可以使用這種方式。token 的缺點(diǎn)就是,由于服務(wù)器不保存 session 狀態(tài),因此無(wú)法在使用過(guò)程中廢止某個(gè) token,或者更改 token 的權(quán)限。也就是說(shuō),一旦 token 簽發(fā)了,在到期之前就會(huì)始終有效,除非服務(wù)器部署額外的邏輯。
目前比較常用的 token 加密方式是 JWT JSON Web Token,關(guān)于 JWT 可以參考阮一峰老師的 JSON Web Token 入門教程
token 刷新
按照上面的 token 邏輯,前端只要保存一個(gè)后端傳過(guò)來(lái)的 token,每次請(qǐng)求附上即可。當(dāng)令牌過(guò)期有兩種選擇,我們可以讓用戶沖洗你登錄,或者后端生成一個(gè)新的令牌,前端保存新的令牌并重新發(fā)送請(qǐng)求。但是這兩種方式都有問(wèn)題,如果讓用戶重新登錄,用戶體驗(yàn)不是很好,頻繁的重新登錄并不是一種比較好的交互方式。而如果自動(dòng)生成新的令牌則會(huì)出現(xiàn)安全問(wèn)題,比如黑客獲取了一個(gè)過(guò)期的令牌并向后端發(fā)送請(qǐng)求,則也可以獲得一個(gè)更新的令牌。
為了權(quán)衡上面的問(wèn)題,產(chǎn)生了一種刷新 token 的機(jī)制,當(dāng)用戶第一次登錄成功,后端會(huì)返回兩個(gè) token,一個(gè) accessToken 用來(lái)進(jìn)行請(qǐng)求,也就是我們每次請(qǐng)求都附上 accessToken,而 refreshToken 則是用來(lái)在 accessToken 過(guò)期的時(shí)候進(jìn)行 accessToken 的刷新。一般來(lái)說(shuō),accessToken 由于每次請(qǐng)求都會(huì)附上,所以安全風(fēng)險(xiǎn)比較高,所以過(guò)期時(shí)間較短,而 refreshToken 則只有在 accessToken 過(guò)期的時(shí)候才會(huì)發(fā)送到后端,所以安全風(fēng)險(xiǎn)相對(duì)較低,所以過(guò)期時(shí)間可以長(zhǎng)一點(diǎn)。
當(dāng)我們的 accessToken 過(guò)期之后,我們會(huì)向后端的 token 刷新接口請(qǐng)求并傳入 refreshToken,后端驗(yàn)證梅雨問(wèn)題之后會(huì)給我們一個(gè)新的 accessToken,我們保存后就可以保證訪問(wèn)的連續(xù)性。當(dāng)然,這也并非絕對(duì)安全的,只是一種相對(duì)安全一點(diǎn)的做法。一般我們將兩個(gè) token 保存在 localStorage 中。
刷新 token 的實(shí)現(xiàn)
在項(xiàng)目中我主要使用的是 axios,所以 token 的刷新以及請(qǐng)求附帶 token 都是使用的 axios 的攔截器完成的。這其中需要注意的地方有三點(diǎn):
- 不要重復(fù)刷新 token,即一個(gè)請(qǐng)求已經(jīng)刷新 token 了,此時(shí)可能新的 token 還沒(méi)有回來(lái),其他請(qǐng)求不應(yīng)該重復(fù)刷新。
- 當(dāng)新的 token 還沒(méi)有回來(lái)的時(shí)候,其他的請(qǐng)求應(yīng)該進(jìn)行暫存,等新的 token 回來(lái)以后再一次進(jìn)行請(qǐng)求。
- 如果請(qǐng)求是由登錄頁(yè)面或者請(qǐng)求本身就是刷新 token 的請(qǐng)求則不需要攔截,否則會(huì)陷入死循環(huán)。
第一個(gè)問(wèn)題用一個(gè) Boolean 字段加鎖即可,第二個(gè)問(wèn)題將請(qǐng)求新 token 過(guò)程中發(fā)起的請(qǐng)求用狀態(tài)為 pendding 的 Promise 進(jìn)行暫存,放到一個(gè)數(shù)組中,當(dāng)新的 token 回來(lái)的時(shí)候依次 resolve 每一個(gè) pendding 的 Promise 即可。具體的代碼細(xì)節(jié)我直接貼上項(xiàng)目上的源碼:
import axios, * as AxiosInterface from 'axios';
// Token 接口,訪問(wèn) token,刷新 token 和過(guò)期時(shí)
const instance = axios.create({
// baseURL: ''
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
async function refreshAccessToken(): Promise<AxiosInterface.AxiosResponse<AxiosData>> {
return await instance.post('api/refreshtoken');
}
let isRefreshing = false;
let requests: Array<Function> = []; // 若在 token 刷新過(guò)程中進(jìn)來(lái)多個(gè)請(qǐng)求則存入 requests 中
// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/';
// 設(shè)置請(qǐng)求攔截器,若 token 過(guò)期則刷新 token
axios.interceptors.request.use(config => {
const tokenObj = JSON.parse(window.localStorage.getItem('token') as string);
if (config.url === 'api/login' || config.url === 'api/refreshtoken') return config;
let accessToken = tokenObj.accessToken;
let expireTime = tokenObj.expireTime;
const refreshToken = tokenObj.refreshToken;
config.headers.Authorization = accessToken;
let time = Date.now();
console.log(time, expireTime);
if (time > expireTime) {
if (!isRefreshing) {
isRefreshing = true;
refreshAccessToken()
.then(res => {
({ accessToken, expireTime } = res.data.data);
time = Date.now();
const tokenStorage = {
accessToken,
refreshToken,
expireTime: Number(time) + Number(expireTime),
};
window.localStorage.setItem('token', JSON.stringify(tokenStorage));
isRefreshing = false;
return accessToken;
})
.then((accessToken: string) => {
requests.forEach(cb => {
cb(accessToken);
});
requests = [];
})
.catch((err: string) => {
throw new Error(`refresh token error: {err}`);
});
}
// 如果是在刷新 token 時(shí)進(jìn)行的請(qǐng)求則暫存在 requests 數(shù)組中,這里需要使用一個(gè) pendding 的 Promise 來(lái)確保攔截的成功
const parallelRequest: Promise<AxiosInterface.AxiosRequestConfig> = new Promise(resolve => {
requests.push((accessToken: string) => {
config.headers.Authorization = accessToken;
console.log(accessToken + Math.random() * 1000);
resolve(config);
});
});
return parallelRequest;
}
return config;
});
export default (vue: Function) => {
vue.prototype.http = axios;
};
總結(jié)
以上就是我對(duì)刷新 token 的實(shí)現(xiàn),如果有什么錯(cuò)誤之處歡迎指正交流。
以上就是token 機(jī)制和實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于token 機(jī)制和實(shí)現(xiàn)方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript的防抖和節(jié)流一起來(lái)了解下
這篇文章主要為大家詳細(xì)介紹了JavaScript的防抖和節(jié)流,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
javascript模擬實(shí)現(xiàn)C# String.format函數(shù)功能代碼
這篇文章主要介紹了javascript模擬實(shí)現(xiàn)C# String.format函數(shù)功能,相信大家可以用的到2013-11-11
Javascript 函數(shù)對(duì)象的多重身份
函數(shù)對(duì)象是javascript 中一個(gè)很特殊的對(duì)象,其特殊體現(xiàn)在他的多重身份上。2009-06-06
淺談layui使用模板引擎動(dòng)態(tài)渲染元素要注意的問(wèn)題
今天小編就為大家分享一篇淺談layui使用模板引擎動(dòng)態(tài)渲染元素要注意的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09
JS實(shí)現(xiàn)問(wèn)卷星自動(dòng)填問(wèn)卷腳本并在兩秒自動(dòng)提交功能
這篇文章主要介紹了JS實(shí)現(xiàn)問(wèn)卷星自動(dòng)填問(wèn)卷腳本兩秒自動(dòng)提交功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2017-08-08

