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



創(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項(xiàng)目
安裝axios
pnpm i axios
src目錄下創(chuàng)建以下兩個(gè)文件
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
/*請(qǐng)求攔截器*/
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處理對(duì)應(yīng)的操作,并返回處理后的message
const message = resolveResError(code, data?.message ?? statusText)
//需要錯(cuò)誤提醒(是否不需要提示)
!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處理對(duì)應(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失效會(huì)跳轉(zhuǎn)下面的catch
return e;
})
if (refreshResponse?.data.accessToken) {
localStorage.setItem('access_token', refreshResponse.data.accessToken);
localStorage.setItem('refresh_token', refreshResponse.data.refreshToken);
// 在原始請(qǐng)求中添加新的access token,并標(biāo)記為重試請(qǐng)求
originalRequest.headers.Authorization = `Bearer ${refreshResponse.accessToken}`;
requesting = false
// 重新發(fā)起請(qǐng)求
return await server(originalRequest);
}
} catch (refreshError) {
// 若刷新token失敗,清除存儲(chǔ)的tokens并通知用戶重新登錄
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
alert('登錄過(guò)期,請(qǐng)重新登錄');
console.log("刷新token失敗");
requesting = false
}
} else {
// 若無(wú)refresh token,直接提示用戶重新登錄
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
console.log("無(wú)刷新token");
alert('登錄過(guò)期,請(qǐng)重新登錄');
}
break;
case 403:
console.log("沒(méi)有權(quán)限");
break;
}
/** 需要錯(cuò)誤提醒 */
!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 = '登錄已過(guò)期,是否重新登錄'
break
case 11007:
case 11008:
message = '退出登錄'
break
case 403:
message = '請(qǐng)求被拒絕'
break
case 404:
message = '請(qǐng)求資源或接口不存在'
break
case 500:
message = '服務(wù)器發(fā)生異常'
break
default:
message = message ?? `【$[code]】: 未知異常!`
break
}
return message
}
根目錄下添加.env配置環(huán)境
VITE_TITLE = '待煎的閑魚(yú)' # 是否使用Hash路由 VITE_USE_HASH = 'true' # 資源公共路徑,需要以 /開(kāi)頭和結(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ù)啟動(dòng)端口號(hào)
// open: true, // 設(shè)置服務(wù)啟動(dòng)時(shí)是否自動(dòng)打開(kāi)瀏覽器
cors: true, // 允許跨域
// 設(shè)置代理,根據(jù)我們項(xiàng)目實(shí)際情況配置
proxy: {
'/api': { //api是自行設(shè)置的請(qǐng)求前綴,按照這個(gè)來(lái)匹配請(qǐng)求,有這個(gè)字段的請(qǐng)求,就會(huì)進(jìn)到代理來(lái)
target: "http://localhost:3000", //是自己需要調(diào)的接口的前綴域名
ws: false,
changeOrigin: true
},
}
}
}
})

以上就是Vue實(shí)現(xiàn)雙token無(wú)感刷新的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue雙token無(wú)感刷新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 前端登錄token失效實(shí)現(xiàn)雙Token無(wú)感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無(wú)感刷新
- 雙Token實(shí)現(xiàn)無(wú)感刷新的完整代碼示例
- 雙token無(wú)感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
- node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無(wú)感刷新機(jī)制的示例
- 雙Token無(wú)感刷新機(jī)制實(shí)現(xiàn)方式
- 前端雙token無(wú)感刷新圖文詳解
- vue中雙token和無(wú)感刷新token的區(qū)別
- Vue3+Vite使用雙token實(shí)現(xiàn)無(wú)感刷新
- SpringBoot+React中雙token實(shí)現(xiàn)無(wú)感刷新
相關(guān)文章
Vscode關(guān)閉Eslint語(yǔ)法檢查的多種方式(保證有效)
eslint是一個(gè)JavaScript的校驗(yàn)插件,通常用來(lái)校驗(yàn)語(yǔ)法或代碼的書(shū)寫風(fēng)格,下面這篇文章主要給大家介紹了關(guān)于Vscode關(guān)閉Eslint語(yǔ)法檢查的多種方式,文章通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
vue+element實(shí)現(xiàn)打印頁(yè)面功能
這篇文章主要介紹了vue+element實(shí)現(xiàn)打印頁(yè)面功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05
使用Vue-Router 2實(shí)現(xiàn)路由功能實(shí)例詳解
vue-router 2只適用于Vue2.x版本,下面我們是基于vue2.0講的如何使用vue-router 2實(shí)現(xiàn)路由功能,需要的朋友可以參考下2017-11-11
vue項(xiàng)目實(shí)現(xiàn)局部全屏完整代碼
最近需要做一個(gè)全屏功能,所以這里給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目實(shí)現(xiàn)局部全屏的相關(guān)資料,需要的朋友可以參考下2023-09-09
vue-router中關(guān)于meta的作用及說(shuō)明
這篇文章主要介紹了vue-router中關(guān)于meta的作用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
vue+element-plus上傳圖片及回顯問(wèn)題及數(shù)量限制
本文主要介紹了vue+element-plus上傳圖片及回顯問(wèn)題及數(shù)量限制,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
解決vue項(xiàng)目F5刷新mounted里的函數(shù)不執(zhí)行問(wèn)題
今天小編就為大家分享一篇解決vue項(xiàng)目F5刷新mounted里的函數(shù)不執(zhí)行問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11

