Vue實現(xiàn)雙token無感刷新的示例代碼
雙token機制,尤其是指在OAuth 2.0授權(quán)協(xié)議中廣泛使用的access token(訪問令牌)和refresh token(刷新令牌)組合,用來實現(xiàn)無感刷新登錄狀態(tài)的原理如下:
初次授權(quán)與發(fā)放Token:
用戶登錄時,通過用戶名、密碼或其他認(rèn)證方式向認(rèn)證服務(wù)器請求授權(quán)。認(rèn)證成功后,服務(wù)器不僅返回一個短期有效的access token(通常幾分鐘到幾小時),還會發(fā)放一個長期有效的refresh token(幾天到幾個月)。
Access Token的作用:
access token是客戶端訪問受保護(hù)資源的臨時憑證,每次客戶端發(fā)起對受保護(hù)資源的請求時,都需要在HTTP請求頭中攜帶access token。一旦access token過期,請求就會失敗。
Refresh Token的作用:
refresh token的目的是在access token過期后,無需用戶重新登錄,客戶端可以使用refresh token向認(rèn)證服務(wù)器申請新的access token。通常refresh token的生命周期較長,而且存儲得更為安全,因為它涉及到長期的授權(quán)。
無感刷新:
當(dāng)客戶端檢測到access token即將過期或已經(jīng)過期時,自動在后臺向認(rèn)證服務(wù)器發(fā)起請求,攜帶refresh token換取新的access token。這個過程對用戶來說是無感知的,即用戶不需要重新登錄,頁面也不會中斷或刷新,因此被稱為“無感刷新”。
安全機制:
為了保證安全性,refresh token一般具備一定的安全措施,例如限制其使用次數(shù)(防止無限刷新)、設(shè)置有效期(過期后必須重新登錄)以及嚴(yán)格的存儲策略(通常不會在客戶端明文存儲,而是存儲在服務(wù)器端或經(jīng)過加密存儲在客戶端本地)。
通過這種雙token機制,可以在保障用戶隱私和安全性的同時,大大提升用戶體驗,讓用戶在長時間操作過程中無需反復(fù)登錄,實現(xiàn)所謂的“無感刷新登錄狀態(tài)”。
后端創(chuàng)建nest項目
# 創(chuàng)建 npx nest new token-test #運行 cd token-test npm run start
AppController 添加login、refresh、getinfo接口
// 登錄請求
@Post('api/login')
login(@Body() userDto: UserDto) {
console.log(userDto);
const user = users.find(item => item.username === userDto.username);
if (!user) {
throw new BadRequestException('用戶不存在');
}
if (user.password !== userDto.password) {
throw new BadRequestException("密碼錯誤");
}
const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});
//access_token 過期時間半小時
const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})
//refresh_token 過期時間 7 天
return {
userInfo: {
username: user.username,
email: user.email
},
accessToken,
refreshToken
};
}
// 刷新token請求
@Post('api/refresh')
refresh(@Body() body: any) {
try {
console.log('refresh token');
console.log(body.token);
const data = this.jwtService.verify(body.token);
const user = users.find(item => item.username === data.username);
const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});
const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})
return {
accessToken,
refreshToken
};
} catch (e) {
throw new UnauthorizedException('token 失效,請重新登錄');
}
}
// 驗證token獲取用戶信息
@Get('api/getinfo')
getinfo(@Req() req: Request) {
const authorization = req.headers['authorization'];
if (!authorization) {
throw new UnauthorizedException('用戶未登錄');
}
try {
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
return {
userInfo: {
username: data.username,
email: data.email
}
};
} catch (e) {
throw new UnauthorizedException('token 失效,請重新登錄');
}
}



創(chuàng)建user.dto.ts
export class UserDto {
username: string;
password: string;
}

AppController添加模擬數(shù)據(jù)
const users = [
{ username: 'test', password: 'success', email: 'abc@163.com' }
]

前端Hbuilder創(chuàng)建VUE3項目
安裝axios
pnpm i axios
src目錄下創(chuàng)建以下兩個文件
utils/request.js
//request.js
import axios from "axios";
import { resolveResError } from "./helpers";
const server = axios.create({
baseURL: "/api",
timeout: 1000 * 10,
headers: {
"Content-type": "application/json"
}
})
var requesting = false
/*請求攔截器*/
function reqResolve(config) {
let accessToken = localStorage.getItem('access_token')
if (accessToken) {
config.headers.Authorization = 'Bearer ' + accessToken
}
return config
}
function reqReject(error) {
return Promise.reject(error)
}
const SUCCESS_CODES = [0, 200, 201, 202, 203, 204, 205]
/*響應(yīng)攔截器*/
function resResolve(response) {
const { data, status, config, statusText, headers } = response
if (headers['content-type']?.includes('json')) {
//獲取狀態(tài)碼
const code = data?.code ?? status
//檢查是否保持
if (SUCCESS_CODES.includes(code)) {
return Promise.resolve(data)
}
// 根據(jù)code處理對應(yīng)的操作,并返回處理后的message
const message = resolveResError(code, data?.message ?? statusText)
//需要錯誤提醒(是否不需要提示)
!config?.noNeedTip && message && window.$message?.error(message)
return Promise.reject({ code, message, error: data ?? response })
}
return Promise.resolve(data ?? response)
}
async function resReject(error) {
if (!error || !error.response) {
const code = error?.code
/** 根據(jù)code處理對應(yīng)的操作,并返回處理后的message */
const message = resolveResError(code, error.message)
window.$message?.error(message)
return Promise.reject({ code, message, error })
}
const { data, status, config } = error.response
const code = data?.code ?? status
const message = resolveResError(code, data?.message ?? error.message)
let originalRequest = error.config;
let refreshToken = localStorage.getItem('refresh_token');
switch (code) {
case 400:
if (message == '用戶不存在') {
return Promise.reject({ code, message, error })
}
break;
case 401:
if (refreshToken && !originalRequest._retry && !requesting) {
originalRequest._retry = true;
requesting = true
try {
// 使用refresh token嘗試獲取新的tokens/
refreshToken = localStorage.getItem('refresh_token');
console.log("刷新refreshToken");
console.log(refreshToken);
const refreshResponse = await axios.post('/api/refresh', {
"token": refreshToken
}).then((res) => {
return res;
}).catch((e) => {
// 刷新token失效會跳轉(zhuǎn)下面的catch
return e;
})
if (refreshResponse?.data.accessToken) {
localStorage.setItem('access_token', refreshResponse.data.accessToken);
localStorage.setItem('refresh_token', refreshResponse.data.refreshToken);
// 在原始請求中添加新的access token,并標(biāo)記為重試請求
originalRequest.headers.Authorization = `Bearer ${refreshResponse.accessToken}`;
requesting = false
// 重新發(fā)起請求
return await server(originalRequest);
}
} catch (refreshError) {
// 若刷新token失敗,清除存儲的tokens并通知用戶重新登錄
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
alert('登錄過期,請重新登錄');
console.log("刷新token失敗");
requesting = false
}
} else {
// 若無refresh token,直接提示用戶重新登錄
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
console.log("無刷新token");
alert('登錄過期,請重新登錄');
}
break;
case 403:
console.log("沒有權(quán)限");
break;
}
/** 需要錯誤提醒 */
!config?.noNeedTip && message && window.$message?.error(message)
return Promise.reject({ code, message, error: error.response?.data || error.response })
}
server.interceptors.request.use(reqResolve, reqReject)
server.interceptors.response.use(resResolve, resReject)
export default server
unitls/helper.js
export function resolveResError(code, message) {
switch (code) {
case 401:
message = '登錄已過期,是否重新登錄'
break
case 11007:
case 11008:
message = '退出登錄'
break
case 403:
message = '請求被拒絕'
break
case 404:
message = '請求資源或接口不存在'
break
case 500:
message = '服務(wù)器發(fā)生異常'
break
default:
message = message ?? `【$[code]】: 未知異常!`
break
}
return message
}
根目錄下添加.env配置環(huán)境
VITE_TITLE = '待煎的閑魚' # 是否使用Hash路由 VITE_USE_HASH = 'true' # 資源公共路徑,需要以 /開頭和結(jié)尾 VITE_PUBLIC_PATH = '/' # 代理配置-target 本地服務(wù) VITE_PROXY_TARGET = 'http://localhost:3000'
根目錄下創(chuàng)建vite.config.js配置代理
import path from 'path'
import { defineConfig, loadEnv } from 'vite'
import Vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const isBuild = command === 'build'
const viteEnv = loadEnv(mode, process.cwd())
const { VITE_TITLE, VITE_PUBLIC_PATH, VITE_PROXY_TARGET } = viteEnv
return {
plugins: [Vue()],
base: VITE_PUBLIC_PATH || '/',
resolve: {
alias: {
'@': path.resolve(process.cwd(), 'src'),
'~': path.resolve(process.cwd()),
},
},
server: {
port: 3200, // 設(shè)置服務(wù)啟動端口號
// open: true, // 設(shè)置服務(wù)啟動時是否自動打開瀏覽器
cors: true, // 允許跨域
// 設(shè)置代理,根據(jù)我們項目實際情況配置
proxy: {
'/api': { //api是自行設(shè)置的請求前綴,按照這個來匹配請求,有這個字段的請求,就會進(jìn)到代理來
target: "http://localhost:3000", //是自己需要調(diào)的接口的前綴域名
ws: false,
changeOrigin: true
},
}
}
}
})

以上就是Vue實現(xiàn)雙token無感刷新的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue雙token無感刷新的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue引用vee-validate插件表單驗證問題(cdn方式引用)
這篇文章主要介紹了Vue引用vee-validate插件表單驗證問題(cdn方式引用),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
Vue項目中使用v-show和v-if指令以及原理、區(qū)別和適用場景說明
Vue中v-show通過display屬性控制顯示隱藏,元素始終存在DOM,適合頻繁切換;v-if條件為false時移除元素,適合初始條件不成立的情況,性能上首次渲染可能更優(yōu),但切換頻繁時v-show更高效,其他方式包括綁定style/class、計算屬性及第三方動畫庫2025-07-07
vant/vue手機端長按事件以及禁止長按彈出菜單實現(xiàn)方法詳解
這篇文章主要介紹了vant/vue手機端長按事件以及禁止長按彈出菜單實現(xiàn)方法詳解,需要的朋友可以參考下2022-12-12
Vue3項目使用PWA技術(shù)進(jìn)行離線加載的詳細(xì)流程
PWA是一種融合了 網(wǎng)頁(Web) 與 原生應(yīng)用(Native App) 優(yōu)點的技術(shù), 它讓你用網(wǎng)頁技術(shù)(HTML、CSS、JavaScript)構(gòu)建出一個能像原生應(yīng)用那樣,簡單來說,就是瀏覽器為你的網(wǎng)頁提供了離線應(yīng)用支持,所以本文給大家介紹了Vue3項目使用PWA技術(shù)進(jìn)行離線加載的過程2025-10-10
vue3.0基于views批量實現(xiàn)動態(tài)路由的方法(示例代碼)
以前vue項目中也有很多實現(xiàn)動態(tài)路由的方法,比如有一些項目涉及權(quán)限的可能會使用api請求路由數(shù)據(jù)在來createRouter,或者本地構(gòu)建使用routes.push來動態(tài)構(gòu)建路由, 今天介紹一種新的方式來基于某個文件夾批量構(gòu)建動態(tài)路由的方法,感興趣的朋友一起看看吧2024-12-12

