Nuxt.js认证中间件异常:自动登出与Toast通知重复问题
问题:Vue.js+Nuxt.js后台管理系统认证中间件异常(自动登出、Toast重复弹出)
我为客户开发了基于Vue.js+Nuxt.js的后台管理系统,后端采用同事开发的Directus。当前认证中间件未按预期工作:登录后将AUTH_TOKEN和REFRESH_TOKEN存储在Cookie中,每5分钟调用API校验token有效性;若返回Token expired则用REFRESH_TOKEN发起刷新请求,更新Cookie中的令牌,失败则跳转至/login页面。但实际使用中出现自动登出、Toast通知重复弹出的问题,相关代码如下:
/middleware/authenticated.js 代码
import authService from '../services/authService'; export default function ({ $cookies, redirect, store, $toast, $router }) { const access_token = $cookies.get('access_token'); const refresh_token = $cookies.get('refresh_token'); if (!access_token) { $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); return redirect('/login'); } if(access_token){ store.commit('SET_USER'); setInterval(function(){ try{ authService.retrieveCurrentUser({headers: { "Content-type": "application/json", "Authorization": `Bearer ${access_token}`, }}) .then(() => { console.log('ok'); }) .catch ((error) => { console.log('prvy catch error', error); if(error.response.data.errors[0].message == 'Token expired.'){ const config = { "refresh_token": refresh_token } authService.refreshToken(config) .then((response) => { $cookies.set('access_token', response.data.data.access_token); $cookies.set('refresh_token', response.data.data.refresh_token); store.commit('SET_USER'); return redirect(); }) .catch((err) => { console.log('druhy catch error', err); $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); $toast.error('Platnosť vášho prihlásenia vypršala, prihláste sa prosím znova.', { timeout: 5000 }); clearInterval(this); return redirect('/login'); }) } }) } catch (err){ console.log('treti catch error', err); $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); $toast.error('Platnosť vášho prihlásenia vypršala, prihláste sa prosím znova.', { timeout: 5000 }); clearInterval(this); return redirect('/login'); } }, 300000) } };
authService 代码
import api from '@/services/api'; export default { login (credentials){ return api().post('/auth/login', credentials); }, refreshToken(config) { return api().post('/auth/refresh', config); }, logout (refresh_token){ return api().post('/auth/logout', refresh_token); }, retrieveCurrentUser(refresh_token){ return api().get('/users/me', refresh_token); } };
页面调用中间件方式
middleware: 'authenticated',
问题根源分析
- 定时器重复创建:每次路由进入触发中间件时,都会新建一个5分钟定时器,导致多个定时器同时运行。token过期时,多个定时器会同时触发刷新逻辑,失败后重复弹出Toast并多次执行登出跳转。
- token变量未实时更新:定时器内使用的是初始化时获取的
access_token,即使刷新后Cookie中的token已更新,定时器仍用旧值发起请求,导致后续校验持续失败,触发自动登出。 - 错误处理冗余无效:外层
try/catch无法捕获axios异步请求的错误(错误会进入axios的catch回调);同时刷新失败后的定时器清理逻辑不严谨,可能导致定时器残留。 - 无意义重定向:刷新token后调用
redirect()会重定向到当前页面,再次触发中间件并新建定时器,形成恶性循环。
修复方案
1. 避免重复创建定时器
在全局对象上标记定时器状态,确保每个会话仅创建一个定时器:
// 修改authenticated.js中定时器创建逻辑 if(access_token){ store.commit('SET_USER'); // 检查是否已存在定时器,避免重复创建 if (!window.authCheckTimer) { window.authCheckTimer = setInterval(async function(){ // 后续校验逻辑 }, 300000) } }
2. 实时获取最新token
每次定时器触发时,从Cookie重新获取token,确保使用最新值:
// 定时器内修改token获取方式 const currentAccessToken = $cookies.get('access_token'); const currentRefreshToken = $cookies.get('refresh_token'); if (!currentAccessToken) { // 无有效token直接登出 clearInterval(window.authCheckTimer); window.authCheckTimer = null; $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); $toast.error('登录已过期,请重新登录', { timeout: 5000 }); return redirect('/login'); } // 使用最新token发起校验请求 authService.retrieveCurrentUser({ headers: { "Content-type": "application/json", "Authorization": `Bearer ${currentAccessToken}`, } })
3. 简化错误处理并严谨清理定时器
改用async/await简化异步逻辑,确保定时器在登出时被彻底清除:
// 重构定时器内逻辑为async/await window.authCheckTimer = setInterval(async function(){ try { const currentAccessToken = $cookies.get('access_token'); const currentRefreshToken = $cookies.get('refresh_token'); if (!currentAccessToken) throw new Error('无有效登录凭证'); await authService.retrieveCurrentUser({ headers: { "Content-type": "application/json", "Authorization": `Bearer ${currentAccessToken}`, } }); console.log('token校验通过'); } catch (error) { console.log('token校验失败:', error); // 处理token过期场景 if (error.response?.data?.errors[0]?.message === 'Token expired.') { const currentRefreshToken = $cookies.get('refresh_token'); try { const response = await authService.refreshToken({ refresh_token: currentRefreshToken }); // 更新Cookie和用户状态 $cookies.set('access_token', response.data.data.access_token); $cookies.set('refresh_token', response.data.data.refresh_token); store.commit('SET_USER'); } catch (err) { // 刷新失败,执行登出 clearInterval(window.authCheckTimer); window.authCheckTimer = null; $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); $toast.error('登录已过期,请重新登录', { timeout: 5000 }); redirect('/login'); } } else { // 其他错误,直接登出 clearInterval(window.authCheckTimer); window.authCheckTimer = null; $cookies.remove('access_token'); $cookies.remove('refresh_token'); store.commit('RESET_USER'); $toast.error('登录已过期,请重新登录', { timeout: 5000 }); redirect('/login'); } } }, 300000)
4. 移除不必要的重定向
刷新token成功后无需调用redirect(),直接更新Cookie和用户状态即可,避免触发中间件重复创建定时器。
5. 统一提示语言
将Toast中的斯洛伐克语提示替换为中文(根据业务需求调整):
$toast.error('登录已过期,请重新登录', { timeout: 5000 });
内容的提问来源于stack exchange,提问作者Marek
相关产品推荐
相关产品推荐

