在現代前端應用中,基于 Token 的身份驗證已成為主流方案。然而,Token 過期問題常常困擾開發者 —— 如何在不打斷用戶操作的情況下自動刷新 Token,實現 "無感刷新" 體驗?本文將詳細介紹基于 Axios 的解決方案。
什么是無感刷新 Token?
無感刷新 Token 指的是當用戶訪問需要身份驗證的接口時,若當前 Token 已過期,系統自動使用刷新 Token 獲取新的訪問 Token,并用新 Token 重新發起原請求,整個過程對用戶完全透明,不影響用戶操作流程。
實現思路
- 攔截所有請求,在請求頭中自動添加 Token
- 攔截響應,檢測 Token 過期錯誤
- 當 Token 過期時,使用刷新 Token 獲取新的訪問 Token
- 用新 Token 重新發起原請求,并將結果返回給用戶
- 處理并發請求問題,避免多次刷新 Token
代碼
auth.js:
// Token存儲工具函數// 存儲Token到本地存儲
export const setToken = (token) => {localStorage.setItem('accessToken', token);
};// 從本地存儲獲取Token
export const getToken = () => {return localStorage.getItem('accessToken');
};// 存儲刷新Token到本地存儲
export const setRefreshToken = (refreshToken) => {localStorage.setItem('refreshToken', refreshToken);
};// 從本地存儲獲取刷新Token
export const getRefreshToken = () => {return localStorage.getItem('refreshToken');
};// 清除所有Token
export const removeTokens = () => {localStorage.removeItem('accessToken');localStorage.removeItem('refreshToken');
};
request.js:
import axios from 'axios';
import { getToken, getRefreshToken, setToken, removeTokens } from './auth';// 創建axios實例
const service = axios.create({baseURL: process.env.VUE_APP_BASE_API, // 基礎URLtimeout: 5000 // 請求超時時間
});// 是否正在刷新的標記
let isRefreshing = false;
// 存儲等待刷新的請求隊列
let requests = [];// 請求攔截器
service.interceptors.request.use(config => {// 從本地存儲獲取Tokenconst token = getToken();// 如果Token存在,則添加到請求頭if (token) {config.headers['Authorization'] = `Bearer ${token}`;}return config;},error => {// 請求錯誤處理return Promise.reject(error);}
);// 響應攔截器
service.interceptors.response.use(response => {// 成功響應處理return response.data;},async error => {const originalRequest = error.config;// 如果不是401錯誤或者已經重試過,則直接返回錯誤if (error.response?.status !== 401 || originalRequest._retry) {return Promise.reject(error);}// 如果正在刷新Token,則將請求加入隊列if (isRefreshing) {try {// 等待刷新Token完成const token = await new Promise(resolve => {requests.push(token => {resolve(token);});});// 使用新Token重新發起請求originalRequest.headers['Authorization'] = `Bearer ${token}`;return service(originalRequest);} catch (err) {return Promise.reject(err);}}// 標記為正在刷新TokenoriginalRequest._retry = true;isRefreshing = true;try {// 調用刷新Token接口const refreshToken = getRefreshToken();const { data } = await axios.post(`${process.env.VUE_APP_BASE_API}/refresh-token`, {refreshToken});// 存儲新的TokensetToken(data.token);// 執行隊列中的請求requests.forEach(cb => cb(data.token));requests = [];// 重新發起原請求originalRequest.headers['Authorization'] = `Bearer ${data.token}`;return service(originalRequest);} catch (refreshError) {// 刷新Token失敗,清除Token并跳轉登錄頁removeTokens();window.location.href = '/login';return Promise.reject(refreshError);} finally {// 重置刷新狀態isRefreshing = false;}}
);export default service;