前端無感刷新token的實(shí)現(xiàn)步驟
Axios 無感知刷新令牌是一種在前端應(yīng)用中實(shí)現(xiàn)自動(dòng)刷新訪問令牌(access token)的技術(shù),確保用戶在進(jìn)行 API 請(qǐng)求時(shí)不會(huì)因?yàn)榱钆七^期而中斷操作
- 訪問令牌(Access Token):用于訪問受保護(hù)資源的憑證,通常有一定的有效期。
- 刷新令牌(Refresh Token):用于獲取新的訪問令牌,當(dāng)訪問令牌過期時(shí)使用。
實(shí)現(xiàn)步驟:
- 設(shè)置攔截器:在 Axios的請(qǐng)求攔截器中添加邏輯,檢查當(dāng)前時(shí)間與令牌的過期時(shí)間。如果訪問令牌已過期但刷新令牌仍然有效,則調(diào)用刷新令牌接口獲取新的訪問令牌。
- 更新令牌存儲(chǔ):一旦獲得新的訪問令牌,將其存儲(chǔ)到 localStorage、Vuex 或其他狀態(tài)管理工具中,以便后續(xù)請(qǐng)求使用新令牌。
- 重試原始請(qǐng)求:在成功刷新令牌后,重新發(fā)送被攔截的請(qǐng)求,此時(shí)使用新的訪問令牌。
XMLHttpRequest
// 創(chuàng)建 XMLHttpRequest 實(shí)例 const xhr = new XMLHttpRequest(); // 登錄成功后保存 Token 和 Refresh Token function onLoginSuccess(response) { localStorage.setItem('accessToken', response.data.accessToken); localStorage.setItem('refreshToken', response.data.refreshToken); } // 發(fā)起請(qǐng)求的函數(shù) function sendRequest(url, method, data) { return new Promise((resolve, reject) => { xhr.open(method, url); xhr.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('accessToken')}`); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(JSON.parse(xhr.responseText)); } else { reject({ status: xhr.status, response: xhr.responseText }); } } }; if (method === 'POST' && data) { xhr.send(JSON.stringify(data)); } else { xhr.send(); } }); } // 刷新 Token 的函數(shù) async function refreshToken() { const refreshToken = localStorage.getItem('refreshToken'); const response = await fetch('/path/to/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token: refreshToken }), }); const res = await response.json(); if (res.success) { localStorage.setItem('accessToken', res.data.newAccessToken); return true; // 表示刷新成功 } else { return false; // 表示刷新失敗 } } // 攔截響應(yīng)并處理 Token 刷新 xhr.addEventListener('readystatechange', function() { if (xhr.readyState === 4 && xhr.status === 401) { refreshToken().then(refreshed => { if (refreshed) { xhr.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('accessToken')}`); xhr.send(); // 重新發(fā)送請(qǐng)求 } else { alert('請(qǐng)重新登錄'); // Token 刷新失敗,可能需要用戶重新登錄 } }); } });
Axios
import axios from 'axios'; // 創(chuàng)建 Axios 實(shí)例 const apiClient = axios.create({ baseURL: 'https://your-api-url.com', // 其他配置... }); // 響應(yīng)攔截器 apiClient.interceptors.response.use(response => { return response; }, error => { const { response } = error; if (response && response.status === 401) { return refreshToken().then(refreshed => { if (refreshed) { // 令牌刷新成功,重試原始請(qǐng)求 return apiClient.request(error.config); } else { // 令牌刷新失敗,可能需要用戶重新登錄 return Promise.reject(error); } }); } return Promise.reject(error); }); // 令牌刷新函數(shù) function refreshToken() { return apiClient.post('/path/to/refresh', { // 刷新令牌所需的參數(shù),例如 refresh_token }).then(response => { if (response.data.success) { // 假設(shè)響應(yīng)數(shù)據(jù)中包含新的訪問令牌 const newAccessToken = response.data.newAccessToken; // 更新令牌存儲(chǔ) localStorage.setItem('accessToken', newAccessToken); // 更新 Axios 實(shí)例的 headers,以便后續(xù)請(qǐng)求使用新令牌 apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; return true; // 表示刷新成功 } else { return false; // 表示刷新失敗 } }); }
Fetch API
// 定義一個(gè)函數(shù)來處理Fetch請(qǐng)求 async function fetchWithToken(url, options = {}) { const token = localStorage.getItem('token'); if (token) { options.headers = { ...options.headers, 'Authorization': `Bearer ${token}` }; } try { const response = await fetch(url, options); if (response.status === 401) { // 假設(shè)401表示令牌過期 const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { throw new Error('No refresh token available'); } // 調(diào)用刷新令牌接口 const refreshResponse = await fetch('/api/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }) }); if (refreshResponse.ok) { const data = await refreshResponse.json(); localStorage.setItem('token', data.newAccessToken); // 重新嘗試原始請(qǐng)求 options.headers['Authorization'] = `Bearer ${data.newAccessToken}`; return fetch(url, options); } else { throw new Error('Failed to refresh token'); } } return response; } catch (error) { console.error('Fetch error:', error); throw error; } } // 使用示例 fetchWithToken('/api/protected-resource') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error));
- fetchWithToken函數(shù): 這是一個(gè)封裝了Fetch API的函數(shù),它首先檢查本地存儲(chǔ)中的訪問令牌是否存在,并在請(qǐng)求頭中添加該令牌。如果響應(yīng)狀態(tài)碼為401(表示令牌過期),則嘗試使用刷新令牌獲取新的訪問令牌,并重新發(fā)送原始請(qǐng)求。
- 刷新令牌邏輯: 在檢測到令牌過期時(shí),函數(shù)會(huì)調(diào)用刷新令牌接口,并將新的訪問令牌存儲(chǔ)到本地存儲(chǔ)中。然后,它會(huì)重新設(shè)置請(qǐng)求頭中的授權(quán)信息,并重新發(fā)送原始請(qǐng)求。
- 錯(cuò)誤處理: 如果在刷新令牌或發(fā)送請(qǐng)求的過程中發(fā)生錯(cuò)誤,函數(shù)會(huì)拋出相應(yīng)的錯(cuò)誤,并在控制臺(tái)中記錄錯(cuò)誤信息。
JQ
// 創(chuàng)建 JQuery 實(shí)例 const apiClient = $.ajaxSetup({ baseURL: 'https://your-api-url.com', // 其他配置... }); // 響應(yīng)攔截器 $.ajaxSetup({ complete: function(jqXHR, textStatus) { if (textStatus === 'error' && jqXHR.status === 401) { return refreshToken().then(refreshed => { if (refreshed) { // 令牌刷新成功,重試原始請(qǐng)求 return apiClient.request(this); } else { // 令牌刷新失敗,可能需要用戶重新登錄 alert('請(qǐng)重新登錄'); } }); } } }); // 令牌刷新函數(shù) function refreshToken() { return $.ajax({ url: '/path/to/refresh', method: 'POST', data: { refresh_token: localStorage.getItem('refreshToken') }, dataType: 'json' }).then(response => { if (response.data.success) { // 假設(shè)響應(yīng)數(shù)據(jù)中包含新的訪問令牌 const newAccessToken = response.data.newAccessToken; // 更新令牌存儲(chǔ) localStorage.setItem('accessToken', newAccessToken); // 更新 JQuery 實(shí)例的 headers,以便后續(xù)請(qǐng)求使用新令牌 apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; return true; // 表示刷新成功 } else { return false; // 表示刷新失敗 } }); }
uni.request
// 導(dǎo)入封裝的request插件 import http from './interface'; import { getRefreshToken } from '@/common/api/apis.js'; // 刷新token接口 let isRefreshing = false; // 是否處于刷新token狀態(tài)中 let fetchApis = []; // 失效后同時(shí)發(fā)送請(qǐng)求的容器 let refreshCount = 0; // 限制無感刷新的最大次數(shù) function onFetch(newToken) { refreshCount += 1; if (refreshCount === 3) { refreshCount = 0; fetchApis = []; return Promise.reject(); } fetchApis.forEach(callback => { callback(newToken); }); // 清空緩存接口 fetchApis = []; return Promise.resolve(); } // 響應(yīng)攔截器 http.interceptor.response((response) => { if (response.config.loading) { uni.hideLoading(); } // 請(qǐng)求成功但接口返回的錯(cuò)誤處理 if (response.data.statusCode && +response.data.statusCode !== 200) { if (!response.config.needPromise) { console.log('error', response); uni.showModal({ title: '提示', content: response.data.message, showCancel: false, confirmText: '知道了' }); // 中斷 return new Promise(() => {}); } else { // reject Promise return Promise.reject(response.data); } } return response; }, (error) => { const token = uni.getStorageSync('token'); const refreshToken = uni.getStorageSync('refreshToken'); // DESC: 不需要做無感刷新的白名單接口 const whiteFetchApi = ['/dealersystem/jwtLogin', '/dealersystem/smsLogin', '/sso2/login', '/dealersystem/isLogin']; switch (error.statusCode) { case 401: case 402: if (token && !whiteFetchApi.includes(error.config.url)) { if (!isRefreshing) { isRefreshing = true; getRefreshToken({ refreshToken }).then(res => { let newToken = res.data; onTokenFetched(newToken).then(res => {}).catch(err => { // 超過循環(huán)次數(shù)時(shí),回到登錄頁,這里可以添加你執(zhí)行退出登錄的邏輯 uni.showToast({ title: '登錄失效,請(qǐng)重新登錄', icon: 'error' }); setTimeout(() => { uni.reLaunch({ url: '/pages/login/login' }); }, 1500); }); }).catch(err => { // refreshToken接口報(bào)錯(cuò),證明refreshToken也過期了,那沒辦法啦重新登錄唄 uni.showToast({ title: '登錄失效,請(qǐng)重新登錄', icon: 'error' }); setTimeout(() => { uni.reLaunch({ url: '/pages/login/login' }); }, 1500); }).finally(() => { isRefreshing = false }); } return new Promise((resolve) => { // 此處的promise很關(guān)鍵,就是確保你的接口返回值在此處resolve,以便后續(xù)代碼執(zhí)行 addFetchApi((newToken) => { error.config.header['Authorization'] = `Bearer ${newToken}`; http.request(error.config).then(response => { resolve(response); }); }); }); } break; default: break; } });
注意事項(xiàng):
- 錯(cuò)誤處理:確保在刷新令牌失敗時(shí),有適當(dāng)?shù)腻e(cuò)誤處理機(jī)制,例如提示用戶重新登錄。
- 并發(fā)請(qǐng)求:處理多個(gè)請(qǐng)求同時(shí)需要刷新令牌的情況,避免重復(fù)刷新。
- 安全性:確保刷新令牌的安全存儲(chǔ)和傳輸,防止被惡意攻擊者獲取。
總結(jié)
到此這篇關(guān)于前端無感刷新token的文章就介紹到這了,更多相關(guān)前端無感刷新token內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
BootStrap的彈出框(Popover)支持鼠標(biāo)移到彈出層上彈窗層不隱藏的原因及解決辦法
彈出框(Popover)與工具提示(Tooltip)類似,提供了一個(gè)擴(kuò)展的視圖。本文給大家介紹BootStrap的彈出框(Popover)支持鼠標(biāo)移到彈出層上彈窗層不隱藏的原因及解決辦法,喜歡的朋友參考下吧2016-04-04javascript設(shè)計(jì)模式 – 備忘錄模式原理與用法實(shí)例分析
這篇文章主要介紹了javascript設(shè)計(jì)模式 – 備忘錄模式,結(jié)合實(shí)例形式分析了javascript備忘錄模式相關(guān)概念、原理、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04jquery自定義插件結(jié)合baiduTemplate.js實(shí)現(xiàn)異步刷新(附源碼)
本文主要介紹了jquery自定義插件結(jié)合baiduTemplate.js實(shí)現(xiàn)異步刷新的具體實(shí)例,具有很好的參考價(jià)值,需要的朋友一起來看下吧2016-12-12ionic2打包android時(shí)gradle無法下載的解決方法
這篇文章主要為大家詳細(xì)介紹了ionic2打包android時(shí)gradle無法下載的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04JavaScript Event Loop相關(guān)原理解析
這篇文章主要介紹了JavaScript Event Loop相關(guān)原理解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06Three.js中調(diào)整相機(jī)視角常用的方法
相機(jī)在Threejs中扮演著至關(guān)重要的角色,它決定了我們從哪個(gè)視角來觀察場景,下面這篇文章主要介紹了Three.js中調(diào)整相機(jī)視角常用的方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-05-05HTML+CSS+JS實(shí)現(xiàn)抓娃娃機(jī)游戲
這篇文章主要介紹了如何利用HTML+CSS+JS制作抓娃娃機(jī)游戲,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04