成人欧美一区二区三区的电影,日韩一级一欧美一级国产,国产成人国拍亚洲精品,无码人妻精品一区二区三区毛片,伊人久久无码大香线蕉综合

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

雙 Token(Access Token + Refresh Token)認(rèn)證機(jī)制:從原理到實(shí)踐的完整實(shí)現(xiàn)

admin
2025年9月26日 9:50 本文熱度 1017
在現(xiàn)代 Web 應(yīng)用中,用戶認(rèn)證是保障系統(tǒng)安全的核心環(huán)節(jié)。隨著前后端分離架構(gòu)的普及,傳統(tǒng)的 Session 認(rèn)證方式逐漸被 Token 認(rèn)證取代。其中,「雙 Token(Access Token + Refresh Token)機(jī)制」憑借其在安全性與用戶體驗(yàn)之間的出色平衡,成為主流的認(rèn)證方案。本文將結(jié)合完整的 Express 后端與 Vue 前端代碼,詳細(xì)解析雙 Token 機(jī)制的實(shí)現(xiàn)原理與實(shí)踐細(xì)節(jié)。

雙 Token 機(jī)制的核心原理

雙 Token 機(jī)制通過兩種不同特性的令牌協(xié)同工作,解決了 "安全性" 與 "用戶體驗(yàn)" 之間的矛盾。其核心設(shè)計(jì)思想是:

  • 「Access Token(訪問令牌)」?:短期有效,用于直接訪問受保護(hù)資源,有效期通常設(shè)置為幾分鐘(示例中為 12 秒,僅用于演示)
  • 「Refresh Token(刷新令牌)」?:長期有效,僅用于獲取新的 Access Token,有效期可設(shè)置為幾天甚至幾周(示例中為 7 天)

這種設(shè)計(jì)的優(yōu)勢在于:當(dāng) Access Token 被盜取時(shí),攻擊者僅有很短的時(shí)間窗口可以利用;而 Refresh Token 雖然長期有效,但通常存儲(chǔ)在更安全的環(huán)境中,且一旦發(fā)現(xiàn)異??闪⒓吹蹁N。

后端實(shí)現(xiàn):Express 框架下的雙 Token 系統(tǒng)

后端作為 Token 的簽發(fā)者和驗(yàn)證者,承擔(dān)著整個(gè)認(rèn)證系統(tǒng)的核心邏輯。以下從初始化配置、核心工具函數(shù)、認(rèn)證中間件到具體接口,逐步解析實(shí)現(xiàn)過程。

基礎(chǔ)配置與依賴

const?express?=?require('express');const?cors?=?require('cors');const?cookieParser?=?require('cookie-parser');const?app?=?express();app.use(cors({? ??origin: [? ? ? ??'http://localhost:5173',?'http://localhost:5174',?? ? ? ??'http://localhost:5175',?'http://localhost:5176'? ? ],? ??credentials:?true?// 允許跨域請(qǐng)求攜帶Cookie}));app.use(express.json());app.use(cookieParser());

這段代碼完成了三件關(guān)鍵工作:

  1. 引入 Express 框架及必要中間件(cors 處理跨域,cookie-parser 解析 Cookie)
  1. 配置跨域規(guī)則,允許指定前端域名訪問并支持跨域攜帶 Cookie
  1. 啟用 JSON 請(qǐng)求體解析和 Cookie 解析功能,為后續(xù)處理打好基礎(chǔ)

令牌存儲(chǔ)與核心工具函數(shù)

// 內(nèi)存存儲(chǔ)(生產(chǎn)環(huán)境應(yīng)使用Redis或數(shù)據(jù)庫)const?accessTokens =?new?Map();const?refreshTokens =?new?Map();// 獲取當(dāng)前時(shí)間戳(秒)function?now() {? ??return?Math.floor(Date.now() /?1000);}// 生成帶前綴的隨機(jī)令牌function?getRandom(prefix) {? ??return?`${prefix}-${Math.random().toString(26).slice(2)}${Date.now()}`;}

為模擬令牌存儲(chǔ),示例使用 Map 對(duì)象臨時(shí)存儲(chǔ)令牌信息。在生產(chǎn)環(huán)境中,應(yīng)替換為 Redis 等分布式存儲(chǔ)系統(tǒng),以支持多實(shí)例部署和令牌過期自動(dòng)清理。

令牌簽發(fā)函數(shù)

function?getAccessToken(userId, ttlSec =?12)?{? ??const?at?=?getRandom('AccessToken');? ? accessTokens.set(at, { userId,?expiresIn:?now() + ttlSec });? ??return?at;}function?getRefreshToken(userId, ttlSec =?3600?*?24?*?7)?{? ??const?rt?=?getRandom('RefreshToken');? ? refreshTokens.set(rt, {?? ? ? ? userId,?? ? ? ??expiresIn:?now() + ttlSec,?? ? ? ??revoked:?false?? ? });? ??return?rt;}

這兩個(gè)函數(shù)分別負(fù)責(zé)生成 Access Token 和 Refresh Token:

  • 為令牌添加前綴便于區(qū)分類型
  • 記錄令牌關(guān)聯(lián)的用戶 ID 和過期時(shí)間
  • 為 Refresh Token 額外添加 "revoked" 狀態(tài),支持主動(dòng)吊銷

令牌驗(yàn)證與吊銷函數(shù)

function verifyAccessToken(at) {? ??const?result = accessTokens.get(at);? ??if?(!result || result.expiresIn <= now())?return?null;? ??return?result.userId;}function verifyRefreshToken(rt) {? ??const?result = refreshTokens.get(rt);? ??if?(!result || result.revoked || result.expiresIn <= now())?return?null;? ??return?result.userId;}function revokeRefreshToken(rt) {? ??const?result = refreshTokens.get(rt);? ??if?(result) result.revoked =?true;}

驗(yàn)證函數(shù)通過檢查令牌是否存在、是否過期、是否被吊銷等狀態(tài),決定是否返回有效的用戶 ID。這種設(shè)計(jì)確保了只有符合條件的令牌才能通過驗(yàn)證。

認(rèn)證中間件:請(qǐng)求的第一道防線

app.use((req, res, next) => {? ??// 登錄、刷新、登出接口跳過驗(yàn)證? ??if?(['/auth/login',?'/auth/refresh',?'/auth/logout',?'/login'].includes(req.path)) {? ? ? ??return?next();? ? }? ??// 從請(qǐng)求頭獲取AT? ??const?token?= req.headers.token ||?'';? ??const?userId?=?verifyAccessToken(token);? ??if?(userId) {? ? ? ? req.userId = userId;?// 掛載用戶ID到請(qǐng)求對(duì)象? ? ? ??return?next();? ? }? ??// 驗(yàn)證失敗? ? res.send({?status:?401,?msg:?"未登錄或令牌過期"?});});

這個(gè)中間件實(shí)現(xiàn)了 "守門人" 功能:

  • 對(duì)登錄、刷新、登出等特殊接口直接放行
  • 對(duì)其他所有請(qǐng)求驗(yàn)證 Access Token 的有效性
  • 驗(yàn)證通過則將用戶 ID 掛載到請(qǐng)求對(duì)象,供后續(xù)接口使用
  • 驗(yàn)證失敗則返回 401 錯(cuò)誤,提示客戶端進(jìn)行處理

核心接口實(shí)現(xiàn)

登錄接口:令牌的初始發(fā)放

app.post('/auth/login', (req, res) => {? ??const?{ username } = req.body || {};? ??const?userId?= username ||?'demoUser';? ??const?at?=?getAccessToken(userId,?12);? ??const?rt?=?getRefreshToken(userId);? ??// 將RT存入httpOnly Cookie? ? res.cookie('rt', rt, {? ? ? ??httpOnly:?true, // 禁止前端JS訪問,防XSS攻擊? ? ? ??sameSite:?'lax', // 限制跨站請(qǐng)求攜帶,防CSRF攻擊? ? ? ??secure:?false, // 本地開發(fā)為false,生產(chǎn)需設(shè)為true? ? ? ??path:?'/',? ? ? ??maxAge:?7?*?24?*?3600?*?1000? ? });? ? res.send({?status:?200,?data: at });});

登錄接口是用戶獲取初始令牌的入口:

  1. 接收用戶身份信息(示例簡化為用戶名)
  1. 生成 Access Token 和 Refresh Token
  1. 將 Access Token 直接返回給前端(通常存儲(chǔ)在 localStorage)
  1. 將 Refresh Token 存入 httpOnly Cookie,提升安全性

特別注意 Cookie 的配置:httpOnly: true防止前端 JavaScript 訪問,有效抵御 XSS 攻擊;sameSite: 'lax'限制跨站請(qǐng)求攜帶 Cookie,降低 CSRF 攻擊風(fēng)險(xiǎn)。

令牌刷新接口:無感續(xù)期的關(guān)鍵

app.post('/auth/refresh', (req, res) => {? ??const?rt?= req.cookies.rt;?// 從Cookie獲取RT? ??if?(!rt)?return?res.send({?status:?401,?msg:?'無刷新令牌'?});? ??const?userId?=?verifyRefreshToken(rt);? ??if?(!userId)?return?res.send({?status:?401,?msg:?'刷新令牌失效'?});? ??// 令牌旋轉(zhuǎn):吊銷舊RT,生成新RT和新AT? ??revokeRefreshToken(rt);? ??const?newRt?=?getRefreshToken(userId);? ??const?newAt?=?getAccessToken(userId);? ??// 寫入新RT到Cookie,返回新AT? ? res.cookie('rt', newRt, { ... });? ? res.send({?status:?200,?data: newAt });});

刷新接口實(shí)現(xiàn)了 Token 的無感續(xù)期:

  1. 從 Cookie 中獲取 Refresh Token 并驗(yàn)證其有效性
  1. 采用 "令牌旋轉(zhuǎn)" 機(jī)制:吊銷舊的 Refresh Token,生成新的一對(duì)令牌
  1. 將新的 Refresh Token 存入 Cookie,新的 Access Token 返回給前端

令牌旋轉(zhuǎn)機(jī)制大幅提升了安全性,即使 Refresh Token 被盜取,攻擊者也只能使用一次。

登出接口:安全終止會(huì)話

app.post('/auth/logout', (req, res) => {? ??const?rt?= req.cookies.rt;? ??if?(rt)?revokeRefreshToken(rt);?// 吊銷RT? ? res.clearCookie('rt', {?path:?'/'?});?// 清除Cookie中的RT? ? res.send({?status:?200,?msg:?'已登出'?});});

登出接口通過吊銷 Refresh Token 并清除 Cookie,確保用戶會(huì)話被安全終止,防止后續(xù)被惡意使用。

前端實(shí)現(xiàn):Vue 中的令牌管理

前端作為令牌的持有者和使用方,需要妥善處理令牌的存儲(chǔ)、傳遞和刷新邏輯。以下從路由守衛(wèi)、請(qǐng)求攔截器到頁面組件,解析前端實(shí)現(xiàn)細(xì)節(jié)。

路由守衛(wèi):控制頁面訪問權(quán)限

router.beforeEach((to,?from, next) =>?{? ??// 處理URL中的token參數(shù)? ??const?token = to.query.token;? ??if?(token) {? ? ? ??localStorage.setItem("token", token);? ? ? ??next({?path: to.path,?query: {} });? ? ? ??return;? ? }? ??// 檢查是否需要認(rèn)證? ??if?(to.meta.requiresAuth) {? ? ? ??const?currentToken =?localStorage.getItem('token');? ? ? ??if?(!isValidToken(currentToken)) {? ? ? ? ? ??// 沒有有效token,跳轉(zhuǎn)到登錄中心? ? ? ? ? ??window.open(`http://localhost:5174/login?resource=${window.location.origin}${to.path}`);? ? ? ? ? ??return;? ? ? ? }? ? }? ??next();})

路由守衛(wèi)實(shí)現(xiàn)了頁面級(jí)別的訪問控制:

  • 處理 URL 中攜帶的 token 參數(shù),存儲(chǔ)到 localStorage
  • 對(duì)標(biāo)記為需要認(rèn)證的路由(如/about),檢查 token 有效性
  • 沒有有效 token 時(shí),引導(dǎo)用戶到登錄頁面

Axios 攔截器:自動(dòng)處理令牌

// 請(qǐng)求攔截器:添加token到請(qǐng)求頭request.interceptors.request.use((config) =>?{? ??const?token =?localStorage.getItem("token");? ? config.headers?= config.headers?|| {}? ??if?(isValidToken(token)){? ? ? ? config.headers.token?= token;? ? }else{? ? ? ??localStorage.removeItem('token');? ? }? ??return?config;})// 響應(yīng)攔截器:處理token過期request.interceptors.response.use(async?(res) => {? ??if?(res.data?&& res.data.status?===?401) {? ? ? ??const?original = res.config?|| {}? ? ? ??if?(original._retried) {? ? ? ? ? ??// 已重試過仍失敗,跳登錄中心? ? ? ? ? ??window.open(`http://localhost:5174/login?resource=${window.location.origin}`)? ? ? ? ? ??return?res;? ? ? ? }? ? ? ? original._retried?=?true? ? ? ??if?(!isRefreshing) {? ? ? ? ? ? isRefreshing =?true? ? ? ? ? ? refreshPromise = request.post('/auth/refresh', {})? ? ? ? ? ? ? ? .then(r?=>?{? ? ? ? ? ? ? ? ? ??if?(r.data?&& r.data.status?===?200) {? ? ? ? ? ? ? ? ? ? ? ??const?newToken = r.data.data? ? ? ? ? ? ? ? ? ? ? ??localStorage.setItem('token', newToken)? ? ? ? ? ? ? ? ? ? ? ??return?newToken? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ??throw?new?Error('refresh failed')? ? ? ? ? ? ? ? })? ? ? ? ? ? ? ? .catch(() =>?{? ? ? ? ? ? ? ? ? ??localStorage.removeItem('token')? ? ? ? ? ? ? ? ? ??throw?new?Error('refresh failed')? ? ? ? ? ? ? ? })? ? ? ? ? ? ? ? .finally(() =>?{? ? ? ? ? ? ? ? ? ? isRefreshing =?false? ? ? ? ? ? ? ? })? ? ? ? }? ? ? ??try?{? ? ? ? ? ??const?newToken =?await?refreshPromise? ? ? ? ? ? original.headers.token?= newToken? ? ? ? ? ??return?request(original)? ? ? ? }?catch?(e) {? ? ? ? ? ??window.open(`http://localhost:5174/login?resource=${window.location.origin}`)? ? ? ? ? ??return?res? ? ? ? }? ? }? ??return?res;})

攔截器是實(shí)現(xiàn) "無感刷新" 的核心:

  • 請(qǐng)求攔截器自動(dòng)為每個(gè)請(qǐng)求添加 Access Token
  • 響應(yīng)攔截器在收到 401 錯(cuò)誤時(shí),自動(dòng)嘗試刷新令牌
  • 使用isRefreshing和refreshPromise避免并發(fā)刷新請(qǐng)求
  • 刷新成功則用新令牌重試原請(qǐng)求,失敗則引導(dǎo)用戶重新登錄

登錄頁面組件:處理身份驗(yàn)證

<script?setup>import?request?from?"../server/request";import?{ useRoute }?from?"vue-router";import?{ watch, ref }?from?"vue";const?route =?useRoute();const?resource =?ref("");const?token =?localStorage.getItem("token");function?windowPostMessage(token, resource) {??if?(window.opener) {? ??window.opener.postMessage({ token }, resource.value)? }}watch(??() =>?route.query.resource,??(val) =>?{? ? resource.value?= val ??decodeURIComponent(val) :?"";? ??if?(token) {? ? ??windowPostMessage(token, resource.value)? ? }? },? {?immediate:?true?});function?login() {? request.get("/auth/login").then((res) =>?{? ??const?apitoken = res.data.data;? ??localStorage.setItem("token", apitoken);? ??windowPostMessage(apitoken, resource.value)? ??window.location.href?=?`${resource.value}?token=${apitoken}`;? ??window.close()? });}</script>

登錄頁面處理用戶身份驗(yàn)證流程:

  • 通過 URL 參數(shù)接收跳轉(zhuǎn)來源(resource)
  • 登錄成功后,通過 postMessage 通知父窗口
  • 將新令牌通過 URL 參數(shù)傳遞給來源頁面
  • 關(guān)閉登錄窗口,完成登錄流程

雙 Token 機(jī)制操作流程演示

為了更直觀地理解雙 Token 機(jī)制的實(shí)際運(yùn)行過程,以下是一個(gè) GIF 演示,展示了從用戶登錄到令牌刷新、訪問資源以及登出的完整操作流程:

演示內(nèi)容說明:

  1. 用戶未登錄時(shí)在Home頁面跳轉(zhuǎn)登錄頁面后輸入信息并登錄,前端獲取 Access Token 并存儲(chǔ),Refresh Token 通過 Cookie 存儲(chǔ)
  1. 登錄后訪問受保護(hù)資源/api1,請(qǐng)求頭攜帶 Access Token,成功獲取資源
  1. 等待 Access Token 過期后再次訪問/api1,前端攔截 401 錯(cuò)誤,自動(dòng)調(diào)用刷新接口獲取新令牌
  1. 使用新的 Access Token 重新請(qǐng)求/api1,成功獲取資源,用戶無感知
  1. 點(diǎn)擊登出按鈕,前端清除本地存儲(chǔ)的 Access Token,后端吊銷 Refresh Token 并清除 Cookie

通過這個(gè)演示可以清晰看到,整個(gè)過程中用戶無需多次輸入賬號(hào)密碼,在 Access Token 過期時(shí)實(shí)現(xiàn)了無感續(xù)期,既保證了安全性又提升了用戶體驗(yàn)。

雙 Token 機(jī)制的安全性考量

雙 Token 機(jī)制的安全性建立在多個(gè)層面的防護(hù)措施上:

  1. 「令牌存儲(chǔ)安全」
  • Access Token 存儲(chǔ)在 localStorage,便于前端管理但存在 XSS 風(fēng)險(xiǎn)
  • Refresh Token 存儲(chǔ)在 httpOnly Cookie,防止前端 JS 訪問,抵御 XSS 攻擊
  1. 「通信安全」
  • 生產(chǎn)環(huán)境應(yīng)啟用 HTTPS,防止令牌在傳輸過程中被竊聽
  • 合理設(shè)置 Cookie 的 secure 屬性,確保僅通過 HTTPS 傳輸
  1. 「令牌生命周期」
  • Access Token 短期有效,減少被盜用后的風(fēng)險(xiǎn)窗口
  • Refresh Token 長期有效但支持主動(dòng)吊銷,平衡安全性與用戶體驗(yàn)
  1. 「防御機(jī)制」
  • 令牌旋轉(zhuǎn)機(jī)制確保 Refresh Token 只能使用一次
  • sameSite Cookie 屬性降低 CSRF 攻擊風(fēng)險(xiǎn)
  • 嚴(yán)格的令牌驗(yàn)證邏輯防止無效令牌被使用

總結(jié)與擴(kuò)展

雙 Token 機(jī)制通過 Access Token 和 Refresh Token 的協(xié)同工作,在安全性和用戶體驗(yàn)之間取得了出色的平衡。本文提供的完整代碼實(shí)現(xiàn)了從令牌簽發(fā)、驗(yàn)證、刷新到登出的全流程,包含了前端和后端的關(guān)鍵處理邏輯。

在實(shí)際應(yīng)用中,還可以進(jìn)一步擴(kuò)展:

  • 使用 Redis 等分布式存儲(chǔ)替換內(nèi)存存儲(chǔ),支持集群部署
  • 實(shí)現(xiàn)令牌黑名單機(jī)制,處理已吊銷但未過期的令牌
  • 添加令牌撤銷通知,在用戶修改密碼等場景立即失效所有令牌
  • 結(jié)合 JWT(JSON Web Token)實(shí)現(xiàn)無狀態(tài)令牌驗(yàn)證,減輕服務(wù)器負(fù)擔(dān)

通過理解和實(shí)踐雙 Token 機(jī)制,開發(fā)者可以為 Web 應(yīng)用構(gòu)建更加安全、可靠的認(rèn)證系統(tǒng),為用戶提供流暢的使用體驗(yàn)同時(shí)保障系統(tǒng)安全。

?

閱讀原文:https://mp.weixin.qq.com/s/vFR_hNx_yBCbdHdiW3kHFA


該文章在 2025/9/26 9:52:54 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲(chǔ)管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved