token 機制和實現方式
前言
之前在面試的時候被問到過重新整理 token 的問題,其實我對 token 驗證機制的細節一直不清楚。新專案和後端的同學商量後使用重新整理 token 來實現。本文主要分享一下對 token 機制的理解和實現方式。
登入驗證的方式
登入驗證一般來說有兩個目的,一個是為了安全,一個是為了使用者方便。因為 HTTP 是無狀態的,所以後端在接受到請求之後並不能知道請求是從哪裡來的,但是很多時候我們有驗證使用者身份的需求,同時前端又有儲存使用者登入狀態的需求。而如果將使用者資訊儲存在前端,必然是非常危險的,很容易被獲取,所以就有了在後端進行非對稱加密的方式來實現登入的驗證和儲存。
目前主要的登入驗證方式有 cookie + session,token,單點登入和 OAuth 第三方登入。本文我們主要講一講 token 登入驗證。
什麼是 token
token 直譯就是令牌的意思,其實就是後端將使用者資訊進行非對稱加密,然後將加密後的內容儲存在前端,當傳送請求的時候帶上這個令牌來實現身份驗證。大致的過程是第一次登入使用者輸入使用者名稱和密碼,伺服器驗證無誤後會對使用者的資訊進行非對稱加密生成一個令牌返回給前端,前端可以存入 cookie 或者 localStorage 等,以後每次傳送請求帶上這個令牌,後端通過對令牌的驗證來識別使用者的身份以及請求的合法性。
token 的優點是服務端不需要儲存 token,只需要驗證前端傳過來的 token 即可,所以幾遍是分散式部署也可以使用這種方式。token 的缺點就是,由於伺服器不儲存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 token 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。
目前比較常用的 token 加密方式是 JWT JSON Web Token,關於 JWT 可以參考阮一峰老師的 JSON Web Token 入門教程
token 重新整理
按照上面的 token 邏輯,前端只要儲存一個後端傳過來的 token,每次請求附上即可。當令牌過期有兩種選擇,我們可以讓使用者沖洗你登入,或者後端生成一個新的令牌,前端儲存新的令牌並重新發送請求。但是這兩種方式都有問題,如果讓使用者重新登入,使用者體驗不是很好,頻繁的重新登入並不是一種比較好的互動方式。而如果自動生成新的令牌則會出現安全問題,比如黑客獲取了一個過期的令牌並向後端傳送請求,則也可以獲得一個更新的令牌。
為了權衡上面的問題,產生了一種重新整理 token 的機制,當用戶第一次登入成功,後端會返回兩個 token,一個 accessToken 用來進行請求,也就是我們每次請求都附上 accessToken,而 refreshToken 則是用來在 accessToken 過期的時候進行 accessToken 的重新整理。一般來說,accessToken 由於每次請求都會附上,所以安全風險比較高,所以過期時間較短,而 refreshToken 則只有在 accessToken 過期的時候才會傳送到後端,所以安全風險相對較低,所以過期時間可以長一點。
當我們的 accessToken 過期之後,我們會向後端的 token 重新整理介面請求並傳入 refreshToken,後端驗證梅雨問題之後會給我們一個新的 accessToken,我們儲存後就可以保證訪問的連續性。當然,這也並非絕對安全的,只是一種相對安全一點的做法。一般我們將兩個 token 儲存在 localStorage 中。
重新整理 token 的實現
在專案中我主要使用的是 axios,所以 token 的重新整理以及請求附帶 token 都是使用的 axios 的攔截器完成的。這其中需要注意的地方有三點:
- 不要重複重新整理 token,即一個請求已經重新整理 token 了,此時可能新的 token 還沒有回來,其他請求不應該重複重新整理。
- 當新的 token 還沒有回來的時候,其他的請求應該進行暫存,等新的 token 回來以後再一次進行請求。
- 如果請求是由登入頁面或者請求本身就是重新整理 token 的請求則不需要攔截,否則會陷入死迴圈。
第一個問題用一個 Boolean 欄位加鎖即可,第二個問題將請求新 token 過程中發起的請求用狀態為 pendding 的 Promise 進行暫存,放到一個數組中,當新的 token 回來的時候依次 resolve 每一個 pendding 的 Promise 即可。具體的程式碼細節我直接貼上專案上的原始碼:
import axios,* as AxiosInterface from 'axios'; // Token 介面,訪問 token,重新整理 token 和過期時 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 重新整理過程中進來多個請求則存入 requests 中 // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'; // 設定請求攔截器,若 token 過期則重新整理 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 時進行的請求則暫存在 requests 陣列中,這裡需要使用一個 pendding 的 Promise 來確保攔截的成功 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; };
總結
以上就是我對重新整理 token 的實現,如果有什麼錯誤之處歡迎指正交流。
以上就是token 機制和實現方式的詳細內容,更多關於token 機制和實現方式的資料請關注我們其它相關文章!