node.js如何實現雙 Token + Cookie 存儲 + 無感刷新機制

node.js如何實現雙 Token + Cookie 存儲 + 無感刷新機制

為什么要實施雙token機制?

優點描述
安全性Access Token 短期有效,降低泄露風險;Refresh Token 權限受限,僅用于獲取新 Token
用戶體驗用戶無需頻繁重新登錄,Token 自動刷新過程對用戶透明
靈活性獨立控制不同 Token 的生命周期,適應各種場景需求
可管理性支持多設備登錄管理,便于撤銷特定設備的登錄狀態
性能優化減少數據庫查詢次數,提升系統響應速度

實現方案:

模塊實現方式
登錄接口返回 accessTokenrefreshToken,分別存入 Cookie
Access Token短時效 JWT,用于請求鑒權
Refresh Token長時效 JWT,用于刷新 Access Token
Token 校驗方式后端從 Cookie 中讀取 token(即 Access Token)
前端 Axios使用響應攔截器統一處理 Token 失效和自動刷新
  • 使用 JWT 生成兩個 Token:
    • Access Token(短時效):用于接口認證,例如有效期為 15 分鐘
    • Refresh Token(長時效):用于刷新 Access Token,例如有效期為 7 天
  • 在用戶登錄時返回這兩個 Token,并將 Refresh Token 存儲在數據庫中
  • 當 Access Token 過期后,客戶端使用 Refresh Token 請求新的 Access Token
  • 如果 Refresh Token 也過期或無效,則強制重新登錄

具體代碼實現

1. 安裝依賴:

cookie-parser用來解析 Cookie 中的 Token

npm install jsonwebtoken bcryptjs cookie-parser

2. 數據庫添加兩個字段

refresh_tokenVARCHAR(255)加密后的 RefreshToken
expires_atDATETIMERefreshToken 過期時間

3. 在后端cors跨域中間中添加屬性

// 將cors注冊為全局中間件
app.use(cors({origin: 'http://localhost:5173', // 前端地址credentials: true // 👈 允許攜帶憑證(cookies)
}))

3. 登錄邏輯改造(添加雙token)

  1. 添加配置文件config.js
module.exports = {jwtSecretKey: 'yke;eky1]239_jwt87-2up34',refreshTokenSecretKey: 'yke;eky1]239_refresh87-2up34',accessExpiresIn: '15m',  // 訪問令牌有效期refreshExpiresIn: '7d',   // 刷新令牌有效期accessExpiresInSec: 15 * 60,  // 秒數refreshExpiresInSec: 7 * 24 * 60 * 60  // 秒數
}
  1. jwt生成accessToken訪問token、refreshToken刷新token
// 生成access token
const accessToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.jwtSecretKey,{ expiresIn: config.accessExpiresIn }
)
// 生成refresh token
const refreshToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.refreshTokenSecretKey,{ expiresIn: config.refreshExpiresIn }
)
  1. 生成token過期時間,和refreshToken一起存入數據庫
 const expiresAt = new Date()expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec)
  1. 將accessToken訪問token、refreshToken刷新token存入cookie
// 設置cookie
res.cookie('token', accessToken, {maxAge: config.accessExpiresInSec * 1000,httpOnly: true,secure: true,path: '/'
})
res.cookie('refresh_token', refreshToken, {maxAge: config.refreshExpiresInSec * 1000,httpOnly: true,secure: true,path: '/api/user/refresh-token',   // 限制路徑提高安全性sameSite: 'none'
})

登錄邏輯完整代碼:

// 用戶登錄的處理函數
exports.login = (req, res) => {// 接收表單數據const userInfo = req.bodyconsole.log(userInfo)// 查詢用戶信息const sqlStr_name = 'select * from user where username=?'db.query(sqlStr_name, [userInfo.username], (err, results) => {if (err) {return res.send({ status: 1, message: err })}// 執行sql語句成功,但是獲取的條數不等于1if (results.length === 0) {return res.send({ status: 1, message: '該用戶不存在' })}// 判斷密碼是否正確const cmpresult = bcrypt.compareSync(userInfo.password, results[0].password)if (!cmpresult) {return res.send({ status: 1, message: '密碼錯誤' })}// 在服務器端生成Token字符串const user = { ...results[0] }// 生成access tokenconst accessToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.jwtSecretKey,{ expiresIn: config.accessExpiresIn })// 生成refresh tokenconst refreshToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.refreshTokenSecretKey,{ expiresIn: config.refreshExpiresIn })// 將refresh token存儲到數據庫中const expiresAt = new Date()expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec)const sqlStr_refreshToken = 'update user set refresh_token=?, expires_at=? where id=?'db.query(sqlStr_refreshToken, [refreshToken, expiresAt, user.id], (err) => {if (err) {console.error('保存refreshToken失敗:', err)return res.send({ status: 1, message: '保存refreshToken失敗' })}// 設置cookieres.cookie('token', accessToken, {maxAge: config.accessExpiresInSec * 1000,httpOnly: true,secure: true,path: '/'})res.cookie('refresh_token', refreshToken, {maxAge: config.refreshExpiresInSec * 1000,httpOnly: true,secure: true,path: '/api/user/refresh-token',   // 限制路徑提高安全性sameSite: 'none'})res.send({status: 0,message: '登錄成功',data: {username: results[0].username}})})  })
}

4. 實現token刷新接口

創建新路由/refreshToken

// token刷新接口
exports.refreshToken = (req, res) => {// 直接從cookie中獲取刷新token => 前端不需要再單獨把token傳入請求頭const refreshToken = req.cookies.refresh_token// 判斷refresh token是否存在if (!refreshToken) {return res.send({ status: 1, message: '缺少refreshToken,請先登錄' })}try {// 驗證refreshTokenconst decoded = jwt.verify(refreshToken, config.refreshTokenSecretKey)// 查詢用戶是否存在且refreshToken匹配const sql = 'select * from user where id=? and refresh_token=?'db.query(sql, [decoded.id, refreshToken], (err, results) => {if (err) {return res.send({ status: 1, message: '無效的refreshToken' + err.message })}const user = results[0]// 生成新的access tokenconst accessToken = jwt.sign({ id: user.id, username: user.username, email: user.email },config.jwtSecretKey,{ expiresIn: config.accessExpiresIn })// 更新accessToken到Cookieres.cookie('token', accessToken, {maxAge: config.accessExpiresInSec * 1000,httpOnly: true,secure: true,path: '/'})res.send({status: 0,message: 'accessToken刷新成功',data: {token: accessToken}})})} catch (error) {return res.status(403).send({ status: 1, message: 'token已過期,請重新登錄' })}
}

5. 響應攔截器中處理token

import axios from 'axios'
import { message } from 'antd'
import { refreshTokenService } from '@/api/user'const instance = axios.create({baseURL: 'http://localhost:3333',  // 你的API服務器地址timeout: 10000,  // 請求超時時間headers: {'Content-Type': 'application/json'},// 必須加上這個選項才能跨域攜帶withCredentials: true
})// 添加請求攔截器
instance.interceptors.request.use((config) => {// 后端將token存在了cookie中,這里不需要攜帶tokenreturn config},(err) => Promise.reject(err)
)// 標記是否正在刷新 Token(防止并發刷新)
let isRefreshing = false
// 保存所有因 Token 失效而等待新 Token 的請求回調函數
let refreshSubscribers = []
// 成功獲取到新的 Token 后,執行所有等待的請求
function onRefreshed(newToken) {refreshSubscribers.forEach((cb) => cb(newToken))refreshSubscribers = []
}
// 將等待刷新 Token 的請求封裝成一個回調函數,加入隊列中
function addRefreshSubscriber(callback) {refreshSubscribers.push(callback)
}
// 響應攔截器
instance.interceptors.response.use((res) => {console.log(res) // 摘取核心響應數據if (res.data.status === 0) {return res}// 處理業務失敗message.error({type: 'error', content: res.data.message || '服務異常'})return Promise.reject(res.data)},async (err) => {// 錯誤的特殊情況 => 401權限不足或token過期 => 攔截到登錄const originalRequest = err.config//  判斷是否是 401 并且不是已經重試過的請求if (err.response?.status === 401 && !originalRequest._retry) {originalRequest._retry = true// 控制 Token 刷新流程(防止多次刷新)if (!isRefreshing) {// 標記刷新狀態isRefreshing = truetry {const res = await refreshTokenService()const newToken = res.data.data.token// 重試請求onRefreshed(newToken)} catch {// 刷新失敗message.error({ type: 'error', content: '登錄已過期,請重新登錄' })// 跳轉登錄if (window.location.pathname !== '/login') {history.push('/login')}} finally {isRefreshing = false}}// 把當前請求放入隊列,等待 Token 刷新后再重發return new Promise((resolve) => {addRefreshSubscriber((newToken) => {originalRequest.headers['Authorization'] = `Bearer ${newToken}`resolve(instance(originalRequest))})})} else {// 錯誤的默認情況 =》 只給提示message.error({ type: 'error', content: err.response.data.message || '服務異常' })}return Promise.reject(err)}
)export default instance

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/906506.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/906506.shtml
英文地址,請注明出處:http://en.pswp.cn/news/906506.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL——6、內置函數

內置函數 1、日期函數2、字符串函數3、數學函數4、其他函數 1、日期函數 1.1、獲取當前日期: 1.2、獲取當前時間: 1.3、獲取當前時間戳: 1.4、獲取當前日期時間: 1.5、提取出日期: 1.6、給日期添加天數或時間…

【Linux】Shell腳本中向文件中寫日志,以及日志文件大小、數量管理

1、寫日志 shell腳本中使用echo命令,將字符串輸入到文件中 覆蓋寫入:echo “Hello, World!” > laoer.log ,如果文件不存在,則會創建文件追加寫入:echo “Hello, World!” >> laoer.log轉移字符:echo -e “Name:\tlaoer\nAge:\t18” > laoer.log,\t制表符 …

深度學習中ONNX格式的模型文件

一、模型部署的核心步驟 模型部署的完整流程通常分為以下階段,用 “跨國旅行” 類比: 步驟類比解釋技術細節1. 訓練模型學會一門語言(如中文)用 PyTorch/TensorFlow 訓練模型2. 導出為 ONNX翻譯成國際通用語言(如英語…

基于兩階段交互控制框架的互聯多能系統協同自治優化

摘要:從多能源集成系統的效益出發,建立了基于交互控制的雙層兩階段框架,以實現互聯多能源系統(MESs)間的最優能量供應。在下層,各MES通過求解成本最小化問題自主確定其可控資產的最優設定值,其中滾動時域優化用于處理負…

matlab編寫的BM3D圖像去噪方法

BM3D(Block-Matching and 3D Filtering)是一種基于塊匹配和三維濾波的圖像去噪方法,廣泛應用于圖像處理領域。它通過在圖像中尋找相似的塊,并將這些塊堆疊成三維數組進行濾波處理,從而有效地去除噪聲,同時保…

前端(小程序)學習筆記(CLASS 1):組件

1、小程序中組件的分類 小程序中的組件也是由宿主環境提供的,開發者可以基于組件快速搭建出漂亮的頁面結構。官方把小程序的組件分為了9大類,分別是: * 視圖容器,* 基礎內容,* 表單組件,* 導航組件 媒體…

基于亞馬遜云科技構建音視頻直播審核方案

1. 前言 隨著互聯網內容形態的多樣化發展,用戶生成內容(UGC)呈現爆發式增長。社交平臺、直播、短視頻、語聊房等應用場景中,海量的音視頻內容需要進行實時審核,以維護平臺安全與用戶體驗。 然而,企業在構…

linux基礎操作11------(運行級別)

一.前言 這個是linux最后一章節內容,主要還是介紹一下,這個就和安全有關系了,內容還是很多的,但是呢,大家還是做個了解就好了。 二.權限掩碼 運行級別 0 關機 運行級別 1 單用戶 ,這個類似于windows安全…

QT+Visual Studio 配置開發環境教程

一、QT架構 Qt Creator 是一個輕量級、跨平臺的 IDE,專為 Qt 開發量身打造,內置對 qmake/CMake 的深度支持、Kits 配置管理、原生 QML 調試器以及較低的資源占用維基百科。 而在 Windows 環境下,Visual Studio 配合 Qt VS Tools 擴展則可將 Q…

(2)JVM 內存模型更新與 G1 垃圾收集器優化

JVM 內存模型更新與 G1 垃圾收集器優化 🚀 掌握前沿技術,成為頂尖 Java 工程師 2?? JVM 內存模型更新 👉 點擊展開題目 JVM內存模型在Java 17中有哪些重要更新?如何優化G1垃圾收集器在容器化環境的表現? &#x1…

TASK04【Datawhale 組隊學習】構建RAG應用

目錄 將LLM接入LangChain構建檢索問答鏈運行成功圖遇到的問題 langchain可以便捷地調用大模型,并將其結合在以langchain為基礎框架搭建的個人應用中。 將LLM接入LangChain from langchain_openai import ChatOpenAI實例化一個 ChatOpenAI 類,實例化時傳入超參數來…

springAI調用deepseek模型使用硅基流動api的配置信息

查看springai的官方文檔,調用deepseek的格式如下: spring.ai.deepseek.api-key${your-api-key} spring.ai.deepseek.chat.options.modeldeepseek-chat spring.ai.deepseek.chat.options.temperature0.8 但是硅基流動的格式不是這樣,這個傘兵…

SpringMVC 通過ajax 實現文件的上傳

使用form表單在springmvc 項目中上傳文件,文件上傳成功之后往往會跳轉到其他的頁面。但是有的時候,文件上傳成功的同時,并不需要進行頁面的跳轉,可以通過ajax來實現文件的上傳 下面我們來看看如何來實現: 方式1&…

Docker安裝Fluentd采集中間件

Fluentd 簡介 :Fluentd 是一個高性能、可擴展的數據收集與聚合工具,能夠統一數據收集和消費,實現各種數據源到各種數據接收器的高效傳輸,廣泛應用于日志收集等領域。 功能特點 : 統一日志收集 :支持從各種…

07SpringMVC底層形象解析

目錄 一、基于餐廳比喻的代碼示例 ,幫助你理解各組件間的協作關系 1. DispatcherServlet 配置(服務員) 2. HandlerMapping 配置(菜單索引) 3. Controller 實現(廚師) 4. Service 層&#x…

eclipse 生成函數說明注釋

在Eclipse中生成函數說明注釋(JavaDoc風格)可以通過以下方法實現: 快捷鍵方式: 將光標放在函數上方輸入/**后按回車鍵Eclipse會自動生成包含參數和返回值的注釋模板 菜單方式: 選中函數點擊菜單欄 Source > Gen…

【題解-洛谷】P6180 [USACO15DEC] Breed Counting S

題目:P6180 [USACO15DEC] Breed Counting S 題目描述 Farmer John 的 N N N 頭奶牛,從左到右編號為 1 …

基于Android的XX校園交流APP

開發語言:Java框架:ssmAndroidJDK版本:JDK1.8服務器:tomcat7數據庫:mysql 5.7數據庫工具:Navicat12開發軟件:eclipse/myeclipse/ideaMaven包:Maven3.3.9 系統展示 APP登錄 APP首頁…

25、工業防火墻 - 工控網絡保護 (模擬) - /安全與維護組件/industrial-firewall-dcs-protection

76個工業組件庫示例匯總 工業防火墻 - 工控網絡保護 (模擬) 概述 這是一個交互式的 Web 組件,旨在模擬工業防火墻在保護關鍵工控網絡(特別是 DCS - 分布式控制系統)免受網絡攻擊(如勒索軟件傳播)方面的核心功能。組件通過可視化簡化的網絡拓撲、模擬網絡流量、應用防火…

kotlin flow的兩種SharingStarted策略的區別

一 兩種 SharingStarted 策略的區別: SharingStarted.Eagerly: 立即開始收集上游流,即使沒有下游訂閱者持續保持活躍狀態,直到 ViewModel 被清除優點:響應更快,數據始終保持最新缺點:消耗更多資源&#x…