vue2 通过路由拦截实现 token 刷新功能
需求场景:系统当中设置的 token 过期时间为 1 小时,当用户在一小时内一直在操作系统,那么当即将一小时 token 快过期时,前端调用刷新 token 的接口,实现为用户的 token 刷新功能。
具体实现流程:
后端:
- 在 nacos 中配置 token 过期时间,及需要刷新 token 的时间
- 在网关层面,控制所有接口的响应头中添加 need-refresh 的状态标识
- 提供一个刷新 token 的接口
前端:
- token 已经过期:跳转到登录页面
- token 即将过期:在路由拦截器中,通过接口请求头中的 need-refresh 标识,判断调用刷新 token 接口的时机
- 特殊处理:为了防止同时进行多个 token 刷新请求,使用了
isRefreshing
标志来控制。只有当isRefreshing
为 false 时,才会开始新的刷新流程。
前端代码实现:
import axios from 'axios';
import store from '@/store';
import router, { resetRouter } from '@/router';
const instance = axios.create({
baseURL: '',
timeout: 10000,
});
let isRefreshing = false;
let pendingRequests = [];
// 添加请求拦截器
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 检查 token 是否即将过期
const needRefresh = config.headers['need-refresh'] == 'true';
if (needRefresh) {
return new Promise((resolve) => {
if (!isRefreshing) {
refreshToken().then((newToken) => {
resolve({
...config,
headers: {
...config.headers,
Authorization: `Bearer ${newToken}`,
},
});
});
} else {
// 如果已经在刷新token,则将请求加入队列
addPendingRequest(config, resolve);
}
});
}
return config;
},
(error) => Promise.reject(error)
);
// token 过期
const tokenExpire = () => {
// token 过期,清除本地缓存,跳转到登录页面
store.dispatch('login/clearStorage');
resetRouter();
// 重置 state 中的值
Object.keys(store.state).forEach((storeName) => {
store.commit(`${storeName}/RESET_STATE`);
});
router.push('/login');
};
// 添加响应拦截器
instance.interceptors.response.use(
(response) => {
const needRefresh = response.headers['need-refresh'] == 'true';
if (response.data && response.data.status == 401) {
tokenExpire();
return;
}
if (needRefresh) {
return refreshToken().then(() => response);
}
return response;
},
async (error) => {
const originalRequest = error.config;
if (
error.response &&
error.response.status === 401 &&
!originalRequest._retry
) {
originalRequest._retry = true;
try {
await refreshToken();
return instance(originalRequest);
} catch (refreshError) {
// 清空pendingRequests,因为 token 刷新失败
pendingRequests = [];
throw refreshError;
}
}
return Promise.reject(error);
}
);
async function refreshToken() {
if (!isRefreshing) {
isRefreshing = true;
try {
const response = await store.dispatch('login/refreshTokenReq');
const { token } = response.data;
localStorage.setItem('token', token);
// 处理挂起的请求
pendingRequests.forEach(({ config, resolve }) => {
resolve({
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
});
});
pendingRequests = []; // 清空队列
} catch (error) {
console.error('Failed to refresh token', error);
// 可能需要重定向到登录页面
tokenExpire();
} finally {
isRefreshing = false;
}
}
}
function addPendingRequest(config, resolve) {
pendingRequests.push({ config, resolve });
}
export default instance;