// api/request.js
import { createAlova } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';
import { message } from 'ant-design-vue';
const REFRESH_TOKEN_URL = '/api/auth/refresh';
let isRefreshing = false;
// 将 Promise 的 resolve 函数存入队列
// 当 token 刷新成功后,调用这些 resolve 函数
let waitingQueue = [];
// 刷新 token 的函数
const refreshToken = async () => {
// 利用 Promise 将并发的刷新请求锁定
if (isRefreshing) {
return new Promise(resolve => {
waitingQueue.push(resolve);
});
}
isRefreshing = true;
try {
const storedRefreshToken = localStorage.getItem('refreshToken');
if (!storedRefreshToken) {
throw new Error('没有刷新令牌');
}
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: storedRefreshToken })
});
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(true)); // 传递 true 表示成功
return true;
} else {
throw new Error(result.message || '刷新令牌失败');
}
} catch (error) {
// 刷新失败,通知所有等待的请求
waitingQueue.forEach(resolve => resolve(false)); // 传递 false 表示失败
// 清理并跳转到登录页
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(error);
} finally {
// 重置状态
isRefreshing = false;
waitingQueue = [];
}
};
// 创建 alova 实例
const alovaInstance = createAlova({
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com',
requestAdapter: GlobalFetch(),
beforeRequest: method => {
// alova 中 method.config 是请求配置
const token = localStorage.getItem('token');
method.config.headers = {
...method.config.headers,
'Content-Type': 'application/json',
Accept: 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
console.log('请求配置:', method.config);
},
responsed: {
// 修改:第二个参数是 method 实例,可以直接用它来重发请求
onSuccess: async (response, method) => {
const json = await response.json();
console.log('响应数据:', json);
if (json.code === 200 || json.code === 0) {
return json.data;
} else if (json.code === 401) {
// 如果是刷新 token 的接口自己返回了 401,则直接跳转登录页,防止死循环
if (method.config.url.includes(REFRESH_TOKEN_URL)) {
message.error('登录已过期,请重新登录');
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(new Error('刷新令牌失败'));
}
// 尝试刷新 token
const refreshSuccess = await refreshToken();
if (refreshSuccess) {
// 刷新成功后,使用 method.send() 重新发送原始请求
// aolva 会自动携带新的 token (因为 beforeRequest 会再次执行)
// 并且重试的请求会再次进入此响应拦截器
return method.send();
} else {
// 刷新失败,Promise 会在 refreshToken 函数内部被 reject
// 这里可以抛出一个错误,防止继续执行
return Promise.reject(new Error("Token refresh failed"));
}
} else {
// 其他业务错误
message.error(json.message || '请求失败');
return Promise.reject(new Error(json.message || '请求失败'));
}
},
onError: (error, method) => {
console.error('请求错误:', error);
// 网络错误等会进入这里
message.error(error.message || '网络错误,请稍后重试');
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