最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue+Node.js實現(xiàn)Token無感刷新的全流程指南

 更新時間:2025年11月13日 09:41:03   作者:拉不動的豬  
這篇文章主要為大家詳細(xì)介紹了Vue+Node.js結(jié)合Axios實現(xiàn)Token無感刷新的全流程,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

頁面基本流程

  • 登錄成功后,后端返回 Access Token 和 Refresh Token,前端存儲兩者及各自有效期。
  • 每次發(fā)起業(yè)務(wù)請求前,前端判斷 Access Token 是否即將過期。
  • 若即將過期,先調(diào)用 “刷新 Token 接口”,用有效的 Refresh Token 換取新的 Access Token。
  • 用新的 Access Token 發(fā)起原業(yè)務(wù)請求,用戶全程無感知。
  • 若 Refresh Token 也過期,才會引導(dǎo)用戶重新登錄。

一、技術(shù)棧與核心約定

前端:Vue 3(適配 Vue 2,只需微調(diào)語法)+ Axios(統(tǒng)一請求攔截)

后端:Node.js + Express + JWT(生成 Token)+ Redis(存儲 Refresh Token,可選但推薦)

Token 規(guī)則:

  • Access Token:短期有效(1 小時),用于業(yè)務(wù)請求身份驗證
  • Refresh Token:長期有效(7 天),僅用于刷新 Access Token
  • 狀態(tài)碼:401 = Access Token 過期 / 無效;403 = Refresh Token 過期 / 無效

二、前端實現(xiàn)(核心代碼)

1. 初始化 Axios 實例(api/index.js)

封裝請求 / 響應(yīng)攔截器,處理 Token 攜帶、刷新和重試邏輯

import axios from 'axios';
import { ElMessage } from 'element-plus'; // 按需引入UI組件庫提示(可選)

// 1. 創(chuàng)建Axios實例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 環(huán)境變量配置后端地址
  timeout: 5000, // 請求超時時間
});

// 2. Token存取工具函數(shù)(安全存儲建議用HttpOnly Cookie,此處用localStorage演示)
const TokenKey = {
  ACCESS: 'access_token',
  REFRESH: 'refresh_token',
};

// 獲取Token
const getAccessToken = () => localStorage.getItem(TokenKey.ACCESS);
const getRefreshToken = () => localStorage.getItem(TokenKey.REFRESH);
// 存儲新Token
const setTokens = (accessToken, refreshToken) => {
  localStorage.setItem(TokenKey.ACCESS, accessToken);
  localStorage.setItem(TokenKey.REFRESH, refreshToken);
};
// 清除Token(退出登錄用)
const removeTokens = () => {
  localStorage.removeItem(TokenKey.ACCESS);
  localStorage.removeItem(TokenKey.REFRESH);
};

// 3. 刷新狀態(tài)管理(防止并發(fā)請求重復(fù)刷新Token)
let isRefreshing = false; // 是否正在刷新Token
let requestQueue = []; // 等待刷新完成的請求隊列

// 4. 請求攔截器:自動給所有請求添加Access Token
service.interceptors.request.use(
  (config) => {
    const token = getAccessToken();
    if (token) {
      // 規(guī)范格式:Bearer + 空格 + Token(后端需對應(yīng)解析)
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 5. 響應(yīng)攔截器:處理Token過期邏輯
service.interceptors.response.use(
  (response) => response.data, // 直接返回響應(yīng)體,簡化業(yè)務(wù)層調(diào)用
  async (error) => {
    const { response, config } = error;
    const originalRequest = config; // 原始失敗請求

    // 僅處理401狀態(tài)碼(Access Token過期/無效),且排除刷新Token本身的請求
    if (response?.status === 401 && originalRequest.url !== '/auth/refresh') {
      // 避免重復(fù)刷新:正在刷新時,將請求加入隊列
      if (isRefreshing) {
        return new Promise((resolve) => {
          requestQueue.push(() => {
            // 刷新成功后,用新Token重試原始請求
            originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`;
            resolve(service(originalRequest));
          });
        });
      }

      originalRequest._retry = true; // 標(biāo)記該請求已進(jìn)入重試流程
      isRefreshing = true; // 開啟刷新狀態(tài)

      try {
        // 調(diào)用后端刷新接口,用Refresh Token換取新Token
        const refreshToken = getRefreshToken();
        if (!refreshToken) {
          throw new Error('Refresh Token不存在');
        }

        const refreshRes = await service.post('/auth/refresh', {
          refreshToken, // 傳給后端的Refresh Token
        });

        // 存儲新Token
        const { accessToken, refreshToken: newRefreshToken } = refreshRes;
        setTokens(accessToken, newRefreshToken);

        // 重試隊列中所有等待的請求
        requestQueue.forEach((callback) => callback());
        requestQueue = []; // 清空隊列

        // 重試當(dāng)前失敗的請求
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return service(originalRequest);
      } catch (refreshError) {
        // 刷新失?。≧efresh Token過期/無效),強制跳轉(zhuǎn)登錄頁
        removeTokens(); // 清除本地?zé)o效Token
        ElMessage.error('登錄已過期,請重新登錄');
        window.location.href = '/login'; // 跳轉(zhuǎn)到登錄頁
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false; // 關(guān)閉刷新狀態(tài)
      }
    }

    // 非401錯誤(如網(wǎng)絡(luò)錯誤、業(yè)務(wù)錯誤),直接拋出
    ElMessage.error(error.message || '請求失敗');
    return Promise.reject(error);
  }
);

export default service;

2. 登錄與業(yè)務(wù)請求示例(api/user.js)

import service from './index';

// 登錄:獲取初始雙Token
export const login = (username, password) => {
  return service.post('/auth/login', { username, password });
};

// 業(yè)務(wù)請求示例(無需手動處理Token)
export const getUserInfo = () => {
  return service.get('/user/info');
};

// 退出登錄:清除Token
export const logout = () => {
  localStorage.removeItem('access_token');
  localStorage.removeItem('refresh_token');
  window.location.href = '/login';
};

3. 登錄頁面使用示例(Login.vue)

<template>
  <div>
    <input v-model="username" placeholder="用戶名" />
    <input v-model="password" type="password" placeholder="密碼" />
    <button @click="handleLogin">登錄</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { login } from '@/api/user';
import { ElMessage } from 'element-plus';

const username = ref('');
const password = ref('');

const handleLogin = async () => {
  try {
    // 調(diào)用登錄接口,后端返回accessToken和refreshToken
    const res = await login(username.value, password.value);
    // 存儲Token(實際已在api攔截器中處理,此處簡化)
    localStorage.setItem('access_token', res.accessToken);
    localStorage.setItem('refresh_token', res.refreshToken);
    ElMessage.success('登錄成功');
    window.location.href = '/home'; // 跳轉(zhuǎn)到首頁
  } catch (error) {
    ElMessage.error('登錄失敗,請檢查賬號密碼');
  }
};
</script>

三、后端實現(xiàn)(Node.js + Express)

1. 依賴安裝

npm install express jsonwebtoken redis cors dotenv // 核心依賴

2. 核心配置(config.js)

require('dotenv').config();

module.exports = {
  // JWT密鑰(生產(chǎn)環(huán)境需用環(huán)境變量,避免硬編碼)
  JWT_SECRET: process.env.JWT_SECRET || 'your-secret-key-321',
  // Token有效期
  ACCESS_TOKEN_EXPIRES: '1h', // 1小時
  REFRESH_TOKEN_EXPIRES: '7d', // 7天
  // Redis配置(存儲Refresh Token,防止重復(fù)使用)
  REDIS: {
    host: 'localhost',
    port: 6379,
    db: 0,
  },
};

3. JWT 工具函數(shù)(utils/jwt.js)

javascript

const jwt = require('jsonwebtoken');
const config = require('../config');

// 生成Token
const generateToken = (payload, expiresIn) => {
  return jwt.sign(payload, config.JWT_SECRET, { expiresIn });
};

// 驗證Token
const verifyToken = (token) => {
  try {
    return jwt.verify(token, config.JWT_SECRET);
  } catch (error) {
    throw new Error('Token無效或已過期');
  }
};

module.exports = { generateToken, verifyToken };

4. Redis 工具函數(shù)(utils/redis.js)

const redis = require('redis');
const config = require('../config');

// 創(chuàng)建Redis客戶端
const client = redis.createClient({
  host: config.REDIS.host,
  port: config.REDIS.port,
  db: config.REDIS.db,
});

// 連接Redis
client.connect().catch((err) => console.error('Redis連接失敗:', err));

// 存儲Refresh Token(key: userId, value: refreshToken)
const setRefreshToken = async (userId, refreshToken) => {
  // 有效期與Refresh Token一致(7天)
  await client.setEx(`refresh_token:${userId}`, 60 * 60 * 24 * 7, refreshToken);
};

// 獲取Refresh Token
const getRefreshToken = async (userId) => {
  return await client.get(`refresh_token:${userId}`);
};

// 刪除Refresh Token(退出登錄時)
const deleteRefreshToken = async (userId) => {
  await client.del(`refresh_token:${userId}`);
};

module.exports = { setRefreshToken, getRefreshToken, deleteRefreshToken };

5. 核心接口實現(xiàn)(routes/auth.js)

const express = require('express');
const router = express.Router();
const { generateToken, verifyToken } = require('../utils/jwt');
const { setRefreshToken, getRefreshToken, deleteRefreshToken } = require('../utils/redis');
const config = require('../config');

// 模擬用戶數(shù)據(jù)庫(實際替換為MySQL/MongoDB)
const mockUsers = [
  { id: 1, username: 'admin', password: '123456' },
];

// 1. 登錄接口:生成雙Token
router.post('/login', (req, res) => {
  const { username, password } = req.body;
  // 驗證賬號密碼
  const user = mockUsers.find(
    (u) => u.username === username && u.password === password
  );

  if (!user) {
    return res.status(400).json({ message: '賬號或密碼錯誤' });
  }

  // 生成雙Token(payload中存儲用戶唯一標(biāo)識,避免敏感信息)
  const accessToken = generateToken({ userId: user.id }, config.ACCESS_TOKEN_EXPIRES);
  const refreshToken = generateToken({ userId: user.id }, config.REFRESH_TOKEN_EXPIRES);

  // 存儲Refresh Token到Redis(用于后續(xù)驗證)
  setRefreshToken(user.id, refreshToken);

  // 返回雙Token給前端
  res.json({
    code: 200,
    message: '登錄成功',
    data: { accessToken, refreshToken },
  });
});

// 2. 刷新Token接口:用有效Refresh Token換取新雙Token
router.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshToken) {
    return res.status(403).json({ message: 'Refresh Token不能為空' });
  }

  try {
    // 1. 驗證Refresh Token有效性
    const payload = verifyToken(refreshToken);
    const { userId } = payload;

    // 2. 驗證Redis中存儲的Refresh Token是否一致(防止偽造)
    const storedRefreshToken = await getRefreshToken(userId);
    if (storedRefreshToken !== refreshToken) {
      return res.status(403).json({ message: 'Refresh Token無效' });
    }

    // 3. 生成新的雙Token
    const newAccessToken = generateToken({ userId }, config.ACCESS_TOKEN_EXPIRES);
    const newRefreshToken = generateToken({ userId }, config.REFRESH_TOKEN_EXPIRES);

    // 4. 更新Redis中的Refresh Token(滑動過期,增強安全性)
    await setRefreshToken(userId, newRefreshToken);

    // 5. 返回新Token
    res.json({
      code: 200,
      data: { accessToken: newAccessToken, refreshToken: newRefreshToken },
    });
  } catch (error) {
    return res.status(403).json({ message: 'Refresh Token已過期,請重新登錄' });
  }
});

// 3. 退出登錄接口:刪除Redis中的Refresh Token
router.post('/logout', async (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(400).json({ message: 'Token不能為空' });
  }

  try {
    const payload = verifyToken(token);
    await deleteRefreshToken(payload.userId);
    res.json({ code: 200, message: '退出登錄成功' });
  } catch (error) {
    res.status(400).json({ message: '退出登錄失敗' });
  }
});

module.exports = router;

6. 后端入口文件(app.js)

const express = require('express');
const cors = require('cors');
const authRouter = require('./routes/auth');

const app = express();
const port = 3001;

// 跨域配置(生產(chǎn)環(huán)境需限制origin)
app.use(cors());
// 解析JSON請求體
app.use(express.json());

// 掛載路由
app.use('/api/auth', authRouter);

// 啟動服務(wù)
app.listen(port, () => {
  console.log(`后端服務(wù)啟動成功:http://localhost:${port}`);
});

四、關(guān)鍵注意事項(生產(chǎn)環(huán)境必看)

安全存儲 Token

  • 不推薦用 localStorage 存儲(易受 XSS 攻擊),優(yōu)先用 HttpOnly Cookie 存儲 Refresh Token,前端無法讀取,避免竊取。
  • Access Token 可存在內(nèi)存(如 Vuex/Pinia),頁面刷新后通過 Cookie 獲取 Refresh Token 重新刷新。

防止重復(fù)刷新

isRefreshing狀態(tài)和requestQueue隊列,避免多個并發(fā)請求同時觸發(fā)刷新接口,導(dǎo)致 Token 沖突。

Redis 的必要性

存儲 Refresh Token 到 Redis,支持 “強制登出”“單點登錄” 功能(如修改密碼后,刪除 Redis 中的舊 Refresh Token,強制用戶重新登錄)。

HTTPS 協(xié)議

生產(chǎn)環(huán)境必須啟用 HTTPS,防止 Token 在傳輸過程中被中間人竊取。

Token 有效期合理設(shè)置

  • Access Token:15 分鐘~2 小時(越短越安全)。
  • Refresh Token:7~30 天(平衡安全性和用戶體驗)。

五、完整流程梳理

用戶登錄 → 后端驗證賬號密碼 → 返回 Access Token 和 Refresh Token → 前端存儲。

前端發(fā)起業(yè)務(wù)請求 → 攔截器自動攜帶 Access Token → 后端驗證有效 → 返回業(yè)務(wù)數(shù)據(jù)。

若 Access Token 過期 → 后端返回 401 → 前端攔截器調(diào)用刷新接口。

刷新接口驗證 Refresh Token 有效 → 返回新雙 Token → 前端更新存儲,重試原始請求。

若 Refresh Token 過期 → 前端清除 Token,跳轉(zhuǎn)登錄頁。

以上就是Vue+Node.js實現(xiàn)Token無感刷新的全流程指南的詳細(xì)內(nèi)容,更多關(guān)于Vue Token無感刷新的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • vue實現(xiàn)日歷表格(element-ui)

    vue實現(xiàn)日歷表格(element-ui)

    這篇文章主要為大家詳細(xì)介紹了vue實現(xiàn)日歷表格(element-ui),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-09-09
  • vue如何在for循環(huán)中設(shè)置ref并獲取$refs

    vue如何在for循環(huán)中設(shè)置ref并獲取$refs

    眾所周知在寫循環(huán)的時候給循環(huán)中的數(shù)據(jù)定義ref以便再下面直接通過this.$ref.來訪問,下面這篇文章主要給大家介紹了關(guān)于vue如何在for循環(huán)中設(shè)置ref并獲取$refs的相關(guān)資料,需要的朋友可以參考下
    2022-12-12
  • vue實現(xiàn)過渡動畫Message消息提示組件示例詳解

    vue實現(xiàn)過渡動畫Message消息提示組件示例詳解

    這篇文章主要為大家介紹了vue實現(xiàn)過渡動畫Message消息提示組件示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • vue如何統(tǒng)一樣式(reset.css與border.css)

    vue如何統(tǒng)一樣式(reset.css與border.css)

    這篇文章主要介紹了vue如何統(tǒng)一樣式(reset.css與border.css),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • 使用Vue.js實現(xiàn)數(shù)據(jù)的雙向綁定

    使用Vue.js實現(xiàn)數(shù)據(jù)的雙向綁定

    在Vue.js中,雙向數(shù)據(jù)綁定是一項非常強大的功能,它能夠使數(shù)據(jù)和視圖之間保持同步,讓開發(fā)者更加方便地操作數(shù)據(jù),在本文中,我們將介紹如何用Vue.js實現(xiàn)數(shù)據(jù)的雙向綁定,需要的朋友可以參考下
    2023-04-04
  • vue-echarts高度縮小時autoresize失效的原因和解決辦法

    vue-echarts高度縮小時autoresize失效的原因和解決辦法

    Vue-Echarts是一個基于ECharts封裝的輕量級、易用的圖表組件庫,它允許你在Vue.js應(yīng)用中方便地集成ECharts,這是一個強大而直觀的數(shù)據(jù)可視化庫,本文給大家介紹了vue-echarts高度縮小時autoresize失效的原因和解決辦法,需要的朋友可以參考下
    2024-12-12
  • 解決iview多表頭動態(tài)更改列元素發(fā)生的錯誤的方法

    解決iview多表頭動態(tài)更改列元素發(fā)生的錯誤的方法

    這篇文章主要介紹了解決iview多表頭動態(tài)更改列元素發(fā)生的錯誤的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-11-11
  • Vite使用Esbuild提升性能詳解

    Vite使用Esbuild提升性能詳解

    這篇文章主要為大家介紹了Vite使用Esbuild提升性能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • vue-element-admin開發(fā)教程(v4.0.0之前)

    vue-element-admin開發(fā)教程(v4.0.0之前)

    本文主要介紹了vue-element-admin開發(fā)教程(v4.0.0之前),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • Vue3實現(xiàn)跨頁面?zhèn)髦档膸追N常見方法

    Vue3實現(xiàn)跨頁面?zhèn)髦档膸追N常見方法

    在Vue 3中,跨頁面?zhèn)髦悼梢酝ㄟ^多種方式實現(xiàn),具體選擇哪種方法取決于應(yīng)用的具體需求和頁面間的關(guān)系,本文列舉了幾種常見的跨頁面?zhèn)髦捣椒?感興趣的同學(xué)跟著小編來看看吧
    2024-04-04

最新評論

泗水县| 胶南市| 印江| 江城| 宁远县| 山丹县| 河北区| 襄城县| 新巴尔虎左旗| 哈尔滨市| 本溪| 蓝山县| 司法| 临沂市| 兴山县| 阿克苏市| 夹江县| 丰原市| 新乡县| 荆州市| 迁西县| 平山县| 玛纳斯县| 黔南| 中西区| 修文县| 遵义市| 莲花县| 深圳市| 那曲县| 大悟县| 湘阴县| 霍林郭勒市| 田阳县| 双峰县| 江西省| 屏山县| 遵化市| 舟山市| 乐业县| 尼木县|