摘要:
Axios 无感知刷新令牌是一种在前端应用中实现自动刷新访问令牌(access token)的技术,确保用户在进行 API 请求时不会因为令牌过期而中断操作
目录概览
-
- XMLHttpRequest
- Axios
- [Fetch API](#Fetch API)
- JQ
- uni.request
- 注意事项:
-
访问令牌(Access Token):用于访问受保护资源的凭证,通常有一定的有效期。
-
刷新令牌(Refresh Token):用于获取新的访问令牌,当访问令牌过期时使用。
实现步骤:
- 设置拦截器:在 Axios的请求拦截器中添加逻辑,检查当前时间与令牌的过期时间。如果访问令牌已过期但刷新令牌仍然有效,则调用刷新令牌接口获取新的访问令牌。
- 更新令牌存储:一旦获得新的访问令牌,将其存储到 localStorage、Vuex 或其他状态管理工具中,以便后续请求使用新令牌。
- 重试原始请求:在成功刷新令牌后,重新发送被拦截的请求,此时使用新的访问令牌。
XMLHttpRequest
bash
// 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
// 登录成功后保存 Token 和 Refresh Token
function onLoginSuccess(response) {
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
}
// 发起请求的函数
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 的函数
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; // 表示刷新失败
}
}
// 拦截响应并处理 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(); // 重新发送请求
} else {
alert('请重新登录'); // Token 刷新失败,可能需要用户重新登录
}
});
}
});
Axios
bash
import axios from 'axios';
// 创建 Axios 实例
const apiClient = axios.create({
baseURL: 'https://your-api-url.com',
// 其他配置...
});
// 响应拦截器
apiClient.interceptors.response.use(response => {
return response;
}, error => {
const { response } = error;
if (response && response.status === 401) {
return refreshToken().then(refreshed => {
if (refreshed) {
// 令牌刷新成功,重试原始请求
return apiClient.request(error.config);
} else {
// 令牌刷新失败,可能需要用户重新登录
return Promise.reject(error);
}
});
}
return Promise.reject(error);
});
// 令牌刷新函数
function refreshToken() {
return apiClient.post('/path/to/refresh', {
// 刷新令牌所需的参数,例如 refresh_token
}).then(response => {
if (response.data.success) {
// 假设响应数据中包含新的访问令牌
const newAccessToken = response.data.newAccessToken;
// 更新令牌存储
localStorage.setItem('accessToken', newAccessToken);
// 更新 Axios 实例的 headers,以便后续请求使用新令牌
apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
return true; // 表示刷新成功
} else {
return false; // 表示刷新失败
}
});
}
Fetch API
bash
// 定义一个函数来处理Fetch请求
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) { // 假设401表示令牌过期
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('No refresh token available');
}
// 调用刷新令牌接口
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);
// 重新尝试原始请求
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函数: 这是一个封装了Fetch API的函数,它首先检查本地存储中的访问令牌是否存在,并在请求头中添加该令牌。如果响应状态码为401(表示令牌过期),则尝试使用刷新令牌获取新的访问令牌,并重新发送原始请求。
- 刷新令牌逻辑: 在检测到令牌过期时,函数会调用刷新令牌接口,并将新的访问令牌存储到本地存储中。然后,它会重新设置请求头中的授权信息,并重新发送原始请求。
- 错误处理: 如果在刷新令牌或发送请求的过程中发生错误,函数会抛出相应的错误,并在控制台中记录错误信息。
JQ
bash
// 创建 JQuery 实例
const apiClient = $.ajaxSetup({
baseURL: 'https://your-api-url.com',
// 其他配置...
});
// 响应拦截器
$.ajaxSetup({
complete: function(jqXHR, textStatus) {
if (textStatus === 'error' && jqXHR.status === 401) {
return refreshToken().then(refreshed => {
if (refreshed) {
// 令牌刷新成功,重试原始请求
return apiClient.request(this);
} else {
// 令牌刷新失败,可能需要用户重新登录
alert('请重新登录');
}
});
}
}
});
// 令牌刷新函数
function refreshToken() {
return $.ajax({
url: '/path/to/refresh',
method: 'POST',
data: {
refresh_token: localStorage.getItem('refreshToken')
},
dataType: 'json'
}).then(response => {
if (response.data.success) {
// 假设响应数据中包含新的访问令牌
const newAccessToken = response.data.newAccessToken;
// 更新令牌存储
localStorage.setItem('accessToken', newAccessToken);
// 更新 JQuery 实例的 headers,以便后续请求使用新令牌
apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
return true; // 表示刷新成功
} else {
return false; // 表示刷新失败
}
});
}
uni.request
bash
// 导入封装的request插件
import http from './interface';
import { getRefreshToken } from '@/common/api/apis.js'; // 刷新token接口
let isRefreshing = false; // 是否处于刷新token状态中
let fetchApis = []; // 失效后同时发送请求的容器
let refreshCount = 0; // 限制无感刷新的最大次数
function onFetch(newToken) {
refreshCount += 1;
if (refreshCount === 3) {
refreshCount = 0;
fetchApis = [];
return Promise.reject();
}
fetchApis.forEach(callback => {
callback(newToken);
});
// 清空缓存接口
fetchApis = [];
return Promise.resolve();
}
// 响应拦截器
http.interceptor.response((response) => {
if (response.config.loading) {
uni.hideLoading();
}
// 请求成功但接口返回的错误处理
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 => {
// 超过循环次数时,回到登录页,这里可以添加你执行退出登录的逻辑
uni.showToast({ title: '登录失效,请重新登录', icon: 'error' });
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' });
}, 1500);
});
}).catch(err => {
// refreshToken接口报错,证明refreshToken也过期了,那没办法啦重新登录呗
uni.showToast({ title: '登录失效,请重新登录', icon: 'error' });
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' });
}, 1500);
}).finally(() => { isRefreshing = false });
}
return new Promise((resolve) => { // 此处的promise很关键,就是确保你的接口返回值在此处resolve,以便后续代码执行
addFetchApi((newToken) => {
error.config.header['Authorization'] = `Bearer ${newToken}`;
http.request(error.config).then(response => {
resolve(response);
});
});
});
}
break;
default:
break;
}
});
注意事项:
- 错误处理:确保在刷新令牌失败时,有适当的错误处理机制,例如提示用户重新登录。
- 并发请求:处理多个请求同时需要刷新令牌的情况,避免重复刷新。
- 安全性:确保刷新令牌的安全存储和传输,防止被恶意攻击者获取。