Vue實(shí)現(xiàn)雙token無感刷新的示例代碼
雙token機(jī)制,尤其是指在OAuth 2.0授權(quán)協(xié)議中廣泛使用的access token(訪問令牌)和refresh token(刷新令牌)組合,用來實(shí)現(xiàn)無感刷新登錄狀態(tài)的原理如下:
初次授權(quán)與發(fā)放Token:
用戶登錄時(shí),通過用戶名、密碼或其他認(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是客戶端訪問受保護(hù)資源的臨時(shí)憑證,每次客戶端發(fā)起對(duì)受保護(hù)資源的請(qǐng)求時(shí),都需要在HTTP請(qǐng)求頭中攜帶access token。一旦access token過期,請(qǐng)求就會(huì)失敗。
Refresh Token的作用:
refresh token的目的是在access token過期后,無需用戶重新登錄,客戶端可以使用refresh token向認(rèn)證服務(wù)器申請(qǐng)新的access token。通常refresh token的生命周期較長(zhǎng),而且存儲(chǔ)得更為安全,因?yàn)樗婕暗介L(zhǎng)期的授權(quán)。
無感刷新:
當(dāng)客戶端檢測(cè)到access token即將過期或已經(jīng)過期時(shí),自動(dòng)在后臺(tái)向認(rèn)證服務(wù)器發(fā)起請(qǐng)求,攜帶refresh token換取新的access token。這個(gè)過程對(duì)用戶來說是無感知的,即用戶不需要重新登錄,頁面也不會(huì)中斷或刷新,因此被稱為“無感刷新”。
安全機(jī)制:
為了保證安全性,refresh token一般具備一定的安全措施,例如限制其使用次數(shù)(防止無限刷新)、設(shè)置有效期(過期后必須重新登錄)以及嚴(yán)格的存儲(chǔ)策略(通常不會(huì)在客戶端明文存儲(chǔ),而是存儲(chǔ)在服務(wù)器端或經(jīng)過加密存儲(chǔ)在客戶端本地)。
通過這種雙token機(jī)制,可以在保障用戶隱私和安全性的同時(shí),大大提升用戶體驗(yàn),讓用戶在長(zhǎng)時(shí)間操作過程中無需反復(fù)登錄,實(shí)現(xiàn)所謂的“無感刷新登錄狀態(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 過期時(shí)間半小時(shí) const refreshToken = this.jwtService.sign({ username: user.username }, { expiresIn: '7d' }) //refresh_token 過期時(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('登錄過期,請(qǐng)重新登錄'); console.log("刷新token失敗"); requesting = false } } else { // 若無refresh token,直接提示用戶重新登錄 localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); console.log("無刷新token"); alert('登錄過期,請(qǐng)重新登錄'); } break; case 403: console.log("沒有權(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 = '登錄已過期,是否重新登錄' 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 = '待煎的閑魚' # 是否使用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ù)啟動(dòng)端口號(hào) // open: true, // 設(shè)置服務(wù)啟動(dòng)時(shí)是否自動(dòng)打開瀏覽器 cors: true, // 允許跨域 // 設(shè)置代理,根據(jù)我們項(xiàng)目實(shí)際情況配置 proxy: { '/api': { //api是自行設(shè)置的請(qǐng)求前綴,按照這個(gè)來匹配請(qǐng)求,有這個(gè)字段的請(qǐng)求,就會(huì)進(jìn)到代理來 target: "http://localhost:3000", //是自己需要調(diào)的接口的前綴域名 ws: false, changeOrigin: true }, } } } })
以上就是Vue實(shí)現(xiàn)雙token無感刷新的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue雙token無感刷新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue移動(dòng)端實(shí)現(xiàn)手指滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了vue移動(dòng)端實(shí)現(xiàn)手指滑動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06Vue3在router中使用pinia報(bào)錯(cuò)的簡(jiǎn)單解決辦法
這篇文章主要給大家介紹了關(guān)于Vue3在router中使用pinia報(bào)錯(cuò)的簡(jiǎn)單解決辦法,什么是pinia,可以理解為狀態(tài)管理工具,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08vue封裝全局彈窗警告組件this.$message.success問題
這篇文章主要介紹了vue封裝全局彈窗警告組件this.$message.success問題,具有很的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09vue中ref標(biāo)簽屬性和$ref的關(guān)系解讀
這篇文章主要介紹了vue中ref標(biāo)簽屬性和$ref的關(guān)系,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07