// api/request.js
import { createAlova } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';
import { message } from 'ant-design-vue'; // 假设使用 Ant Design Vue 的消息提示
// 刷新 Token 的接口地址
const REFRESH_TOKEN_URL = '/api/auth/refresh';
// 是否正在刷新 token 的标记
let isRefreshing = false;
// 等待刷新 token 的请求队列
let waitingQueue = [];
// 刷新 token 的函数
const refreshToken = async () => {
// 防止多次刷新
if (isRefreshing) {
return new Promise((resolve) => {
// 将请求加入队列
waitingQueue.push(resolve);
});
}
isRefreshing = true;
try {
// 获取刷新 token
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('没有刷新令牌');
}
// 发起刷新 token 请求
const response = await fetch(import.meta.env.VITE_API_BASE_URL + REFRESH_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken })
});
const result = await response.json();
if (result.code === 200 || result.code === 0) {
// 更新存储的 token
localStorage.setItem('token', result.data.token);
localStorage.setItem('refreshToken', result.data.refreshToken);
// 执行队列中的请求
waitingQueue.forEach(resolve => resolve(result.data.token));
waitingQueue = [];
return result.data.token;
} else {
throw new Error(result.message || '刷新令牌失败');
}
} catch (error) {
// 刷新失败,清空队列并返回失败
waitingQueue.forEach(resolve => resolve(null));
waitingQueue = [];
// 清除登录状态
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
// 跳转到登录页
window.location.href = '/login';
throw error;
} finally {
isRefreshing = false;
}
};
// 创建 alova 实例
const alovaInstance = createAlova({
// 请求基础路径
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com',
// 请求适配器
requestAdapter: GlobalFetch(),
// 全局的请求拦截器
beforeRequest: (config) => {
// 获取 token
const token = localStorage.getItem('token');
// 设置请求头
config.headers = {
...config.headers,
'Content-Type': 'application/json',
'Accept': 'application/json',
// 如果有 token,添加到请求头
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
// 请求日志
console.log('请求配置:', config);
return config;
},
// 全局的响应拦截器
responsed: {
// 请求成功的拦截器
onSuccess: async (response, method, config) => {
const json = await response.json();
// 打印响应数据
console.log('响应数据:', json);
// 约定的接口响应格式: { code: number, data: any, message: string }
if (json.code === 200 || json.code === 0) {
// 成功响应,返回数据部分
return json.data;
} else if (json.code === 401) {
// 未授权,尝试刷新 token
try {
// 如果不是刷新 token 的请求,则尝试刷新 token
if (!config.url.includes(REFRESH_TOKEN_URL)) {
// 刷新 token
const newToken = await refreshToken();
if (newToken) {
// 使用新 token 重新发起原请求
const newConfig = { ...config };
newConfig.headers = {
...newConfig.headers,
'Authorization': `Bearer ${newToken}`
};
// 重新发起请求
const retryResponse = await fetch(newConfig.url, {
method: newConfig.method,
headers: newConfig.headers,
body: newConfig.data ? JSON.stringify(newConfig.data) : undefined
});
const retryJson = await retryResponse.json();
if (retryJson.code === 200 || retryJson.code === 0) {
return retryJson.data;
} else {
message.error(retryJson.message || '请求失败');
return Promise.reject(new Error(retryJson.message || '请求失败'));
}
}
}
// 如果是刷新 token 的请求失败,或者刷新 token 后重试仍然失败
message.error('登录已过期,请重新登录');
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(new Error(json.message || '登录已过期'));
} catch (error) {
console.error('刷新 token 失败:', error);
message.error('登录已过期,请重新登录');
return Promise.reject(error);
}
} else {
// 其他业务错误
message.error(json.message || '请求失败');
return Promise.reject(new Error(json.message || '请求失败'));
}
},
// 请求失败的拦截器
onError: (error, method) => {
console.error('请求错误:', error);
message.error('网络错误,请稍后重试');
return Promise.reject(error);
}
},
// 请求超时时间,单位为毫秒
timeout: 10000,
});
// 导出实例
export default alovaInstance;
// api/modules/auth.js
// 认证相关接口封装
import alovaInstance from '../request';
export const authApi = {
// 用户登录
login: (data) => {
return alovaInstance.Post('/api/auth/login', data);
},
// 手动刷新 token(通常不需要直接调用,由拦截器自动处理)
refreshToken: (refreshToken) => {
return alovaInstance.Post('/api/auth/refresh', { refreshToken });
},
// 用户登出
logout: () => {
return alovaInstance.Post('/api/auth/logout');
}
};
// 登录组件示例
<script setup>
import { ref } from 'vue';
import { authApi } from '@/api';
import { useRouter } from 'vue-router';
const router = useRouter();
const username = ref('');
const password = ref('');
const loading = ref(false);
const handleLogin = async () => {
if (!username.value || !password.value) {
return;
}
loading.value = true;
try {
const result = await authApi.login({
username: username.value,
password: password.value
});
// 存储 token 和 refreshToken
localStorage.setItem('token', result.token);
localStorage.setItem('refreshToken', result.refreshToken);
// 登录成功,跳转到首页
router.push('/dashboard');
} catch (error) {
console.error('登录失败:', error);
} finally {
loading.value = false;
}
};
</script>
主要功能说明
1. Token 刷新机制
-
Token 刷新功能:
- 当接口返回 401 时,自动尝试刷新 Token
- 使用 refreshToken 调用刷新接口获取新的 Token
- 刷新成功后,使用新 Token 重新发起原请求
-
并发请求处理:
- 使用
isRefreshing
标记防止多个请求同时触发刷新 - 使用
waitingQueue
队列存储等待 Token 刷新的请求 - Token 刷新成功后,自动执行队列中的所有请求
- 使用
-
异常处理:
- 如果 Token 刷新失败,会清除登录状态并跳转到登录页
- 如果刷新后的重试请求仍然失败,会返回错误信息
2. 使用说明
-
登录时需要保存两个 Token:
token
:用于日常接口认证refreshToken
:用于刷新获取新的 token
-
刷新 Token 接口约定:
- 接口地址:
/api/auth/refresh
- 请求参数:
{ refreshToken: 'xxx' }
- 返回格式:
{ code: 0, data: { token: 'xxx', refreshToken: 'xxx' }, message: 'success' }
- 接口地址:
-
自动处理流程:
- 接口返回 401 → 尝试刷新 Token → 刷新成功 → 重新发起原请求 → 返回结果
- 接口返回 401 → 尝试刷新 Token → 刷新失败 → 跳转登录页
这种方式可以让用户在 Token 过期时无感知地继续操作,大大提升用户体验。
这段代码确实是比较巧妙的并发处理机制,我来详细解释一下:
详细解释
当多个请求同时遇到 401 错误时,我们只希望发起一次 token 刷新请求,而不是每个请求都去刷新 token。这段代码的目的就是实现这一点。
// 防止多次刷新
if (isRefreshing) {
return new Promise((resolve) => {
// 将请求加入队列
waitingQueue.push(resolve);
});
}
让我们分析一下具体流程:
-
假设有多个请求(A、B、C)同时或几乎同时收到 401 错误
-
请求 A 先执行到这段代码,此时
isRefreshing = false
,所以它会跳过这个 if 语句,继续执行下面的代码去刷新 token,并设置isRefreshing = true
-
请求 B 执行到这段代码时,发现
isRefreshing = true
(因为请求 A 已经在刷新 token),所以它会进入 if 语句 -
在 if 语句中,创建了一个新的 Promise,并将这个 Promise 的
resolve
函数添加到waitingQueue
队列中 -
这个 Promise 会一直处于 pending 状态,直到有人调用它的
resolve
函数 -
当请求 A 成功刷新 token 后,会遍历
waitingQueue
队列,调用里面的每个resolve
函数,并传入新的 token:waitingQueue.forEach(resolve => resolve(result.data.token));
-
这时,请求 B 的 Promise 状态从 pending 变为 fulfilled,它可以继续执行后续代码,使用新的 token 重新发起请求
简单来说
waitingQueue.push(resolve)
添加的是一个函数,这个函数可以让当前 Promise 从等待状态变为完成状态- 当第一个请求成功刷新 token 后,会调用队列中所有的这些函数,告诉其他等待的请求:"token 已经刷新好了,这是新 token,你们可以继续了"
- 其他请求拿到新 token 后,就可以重新发起原来的请求,而不需要自己再去刷新 token