雙token無感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
雙token無感刷新是一種在Web開發(fā)中常用的安全及用戶體驗(yàn)優(yōu)化技術(shù)。以下是對(duì)雙token無感刷新的詳細(xì)解釋:
一、基本概念
雙token無感刷新主要涉及兩種類型的token:Access Token(訪問令牌)和Refresh Token(刷新令牌)。
- Access Token:用戶直接用于訪問受保護(hù)資源的憑證。有效期較短,通常幾分鐘到幾小時(shí)不等。一旦過期,用戶需要重新認(rèn)證以獲取新的Access Token。即使Access Token被泄露,由于其有效期短,攻擊者利用它進(jìn)行不當(dāng)操作的時(shí)間窗口有限。
- Refresh Token:用于在Access Token過期后或過期前的一定時(shí)間內(nèi)重新獲取新的Access Token。有效期通常較長(zhǎng),甚至可以說是永久的,但出于安全考慮,一般會(huì)設(shè)置過期時(shí)間或使用次數(shù)限制。Refresh Token通常不會(huì)直接發(fā)送給客戶端,而是保存在服務(wù)器端或經(jīng)過加密后存儲(chǔ)在客戶端本地(如localStorage或sessionStorage)。
二、工作原理
雙token無感刷新的核心在于自動(dòng)在后臺(tái)處理Access Token的過期和刷新過程,而無需用戶重新登錄或感知到這一過程。具體流程如下:
- 用戶通過用戶名(賬號(hào))、密碼或其他認(rèn)證方式向認(rèn)證服務(wù)器請(qǐng)求授權(quán)。
- 認(rèn)證成功后,服務(wù)器返回Access Token和Refresh Token給前端。前端將這兩個(gè)Token保存到本地存儲(chǔ)中,以便在需要時(shí)使用。
- 前端在訪問受保護(hù)資源時(shí),將Access Token放入請(qǐng)求頭中發(fā)送給后端。
- 如果Access Token有效,后端正常處理請(qǐng)求并返回結(jié)果。
- 如果Access Token過期,后端會(huì)返回一個(gè)錯(cuò)誤響應(yīng)(如HTTP 401 Unauthorized)。
- 前端在接收到錯(cuò)誤響應(yīng)后,自動(dòng)在后臺(tái)使用Refresh Token向認(rèn)證服務(wù)器請(qǐng)求新的Access Token。
- 認(rèn)證服務(wù)器驗(yàn)證Refresh Token的有效性后,返回一個(gè)新的Access Token和(可選的)新的Refresh Token。
- 前端更新本地存儲(chǔ)的Access Token和Refresh Token,并重新發(fā)起之前的請(qǐng)求。
三、實(shí)現(xiàn)方式
雙token無感刷新的實(shí)現(xiàn)通常依賴于前端和后端的配合。以下是一個(gè)簡(jiǎn)化的實(shí)現(xiàn)流程:
前端實(shí)現(xiàn):
- 設(shè)置全局請(qǐng)求攔截器,在發(fā)送請(qǐng)求前檢查Access Token的有效性。
- 如果Access Token即將過期或已過期,自動(dòng)使用Refresh Token請(qǐng)求新的Access Token。
- 更新本地存儲(chǔ)中的Access Token和Refresh Token。
- 重新發(fā)起因Access Token過期而失敗的請(qǐng)求。
后端實(shí)現(xiàn):
- 提供一個(gè)用于刷新Token的接口,該接口接收Refresh Token作為參數(shù)。
- 驗(yàn)證Refresh Token的有效性,如果有效,則生成新的Access Token和(可選的)新的Refresh Token,并返回給客戶端。
- 在受保護(hù)資源的訪問接口中驗(yàn)證Access Token的有效性,并在其過期時(shí)返回相應(yīng)的錯(cuò)誤響應(yīng)。
四、優(yōu)勢(shì)與應(yīng)用場(chǎng)景
雙token無感刷新的優(yōu)勢(shì)在于提高了系統(tǒng)的安全性和用戶體驗(yàn)。通過分離短期和長(zhǎng)期的憑證,降低了Access Token被泄露的風(fēng)險(xiǎn),同時(shí)避免了用戶因Token過期而頻繁重新登錄的麻煩。
這一機(jī)制廣泛應(yīng)用于需要高安全性和良好用戶體驗(yàn)的場(chǎng)景,如Web應(yīng)用和移動(dòng)應(yīng)用。在這些場(chǎng)景中,雙token無感刷新能夠提升用戶在持續(xù)操作過程中的連貫性和安全性。
五、后端代碼解析(nodejs+express)
(1)app.js引入所需要的模塊
//app.js var createError = require('http-errors'); //用于創(chuàng)建各種HTTP錯(cuò)誤 var express = require('express'); //引入express模塊 var path = require('path'); //用于處理文件和目錄的路徑 var cookieParser = require('cookie-parser'); //解析HTTP請(qǐng)求中的Cookie頭 var logger = require('morgan'); //用于將請(qǐng)求日志輸出到stdout或指定文件 var indexRouter = require('./routes/index'); //主路由 var usersRouter = require('./routes/users'); //用戶相關(guān)路由 var jwt = require("jsonwebtoken"); //引入jsonwebtoken模塊 var app = express(); var cors = require("cors") app.use(cors()) //處理跨域
(2)自定義中間件,用于處理jwt驗(yàn)證
//app.js app.use((req, res, next) => { // 定義不需要驗(yàn)證的路徑 let pathArr = [ '/userLogin', '/refresh', '/sendYzm', '/resetPassword', '/findUser', '/upload' ] // 如果請(qǐng)求路徑在不需要驗(yàn)證的路徑中,直接調(diào)用next()繼續(xù)處理 if (pathArr.includes(req.path)) { return next() } // 獲取請(qǐng)求頭中的accessToken和refreshToken const accessToken = req.headers.accesstoken const refreshToken = req.headers.refreshtoken // 判斷refreshToken是否過期 try { jwt.verify(refreshToken, "MTHGH") } catch (error) { console.log(error); return res.status(403).send({ message: 'refreshToken驗(yàn)證失敗' }) } // 如果沒有accessToken 返回401 if (!accessToken) { return res.status(401).send({ message: '未獲取到accessToken' }) } // 驗(yàn)證accessToken try { const user = jwt.verify(accessToken, "MTHGH") res.locals.user = user//將用戶信息存儲(chǔ)在res.locals中,供后續(xù)中間件使用 return next() } catch (error) { return res.status(401).send({ message: 'accessToken驗(yàn)證失敗' }) } })
(3)index.js所需引入模塊
// index.js var express = require('express'); //引入了Express模塊 var router = express.Router(); //用于處理HTTP請(qǐng)求 const jwt = require('jsonwebtoken'); //實(shí)現(xiàn)用戶身份驗(yàn)證和授權(quán)等功能
(4)封裝生成長(zhǎng)token和短token的函數(shù)
// index.js // 訪問令牌有效期 const ACCESS_TOKEN_EXPIRATION='1h' // 刷新令牌有效期 const REFRESH_TOKEN_EXPIRATION='1d' // 令牌密鑰 const SECRET_KEY="MTHGH" // 這里創(chuàng)建了一個(gè)Map對(duì)象,用于存儲(chǔ)用戶名和對(duì)應(yīng)的刷新令牌列表。這樣,當(dāng)用戶登錄或刷新令牌時(shí),可以跟蹤和驗(yàn)證他們的刷新令牌。 const refreshTokenMap=new Map() // 生成函數(shù)令牌 function generateToken(name,expiration){ return jwt.sign({name},SECRET_KEY,{expiresIn:expiration}) } // 封裝生成長(zhǎng)token和短token function getToken(name){ let accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) let refreshToken = generateToken(name,REFRESH_TOKEN_EXPIRATION) const refreshTokens = refreshTokenMap.get(name)||[] refreshTokens.push(refreshToken) refreshTokenMap.set(name,refreshTokens) return { accessToken, refreshToken } }
(5)賬號(hào)密碼登錄
// index.js router.post('/userLogin', async (req, res) => { // 獲取前端傳遞的數(shù)據(jù) const {username,password} = req.body // 通過用戶查找是否存在 let user = await userModel.findOne({userName:username}) // 不存在 if(!user){ return res.status(200).send({message:"賬號(hào)錯(cuò)誤",code:1}) } // 用戶存在,密碼不正確 if(user.passWord!==password){ return res.status(200).send({message:"密碼錯(cuò)誤",code:2}) } // 調(diào)用函數(shù),生成token let {accessToken,refreshToken}=getToken(user.userName) // 返回用戶、token res.status(200).send({ data:user, accessToken, refreshToken, message:'登陸成功', code:200 }) })
(6)刷新短token
// index.js router.get('/refresh',async(req,res)=>{ // 獲取請(qǐng)求頭的token信息 const refreshToken = req.headers.refreshtoken // token不存在 if(!refreshToken){ res.status(403).send("沒有短token") } // 驗(yàn)證token是否過期 try{ const {name} = jwt.verify(refreshToken,SECRET_KEY) const accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) res.status(200).send({accessToken}) }catch(error){ console.log('長(zhǎng)token已經(jīng)過期'); res.status(403).send('長(zhǎng)token已經(jīng)過期') } })
六、前端代碼解析(React)
(1)添加axios重試機(jī)制
// api.jsx let retryCount = 0; // 初始化重試計(jì)數(shù) const customRetryCondition = async (error) => { // 自定義重試條件 if (axios.isAxiosError(error) && error.response?.status !== 200) { // 如果是 Axios 錯(cuò)誤且響應(yīng)狀態(tài)不是 200 if (error.response?.status === 403) { // 如果后端返回 403(禁止訪問) localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken console.log('請(qǐng)重新登錄'); // 打印提示信息 window.location.href='/login' // 跳轉(zhuǎn)到登錄頁(yè)面 return false; // 不重試 } if (error.response?.status === 401) { // 如果后端返回 401(未授權(quán)) await refresh(); // 嘗試刷新 token console.log('刷新token'); // 打印提示信息 return true; // 允許重試 } retryCount++; // 增加重試計(jì)數(shù) console.log(`第${retryCount}次重試`); // 打印當(dāng)前重試次數(shù) return ( error.response.status >= 500 || // 如果響應(yīng)狀態(tài)是 500 或以上 (error.response.status < 500 && error.response?.status !== 401) // 或者狀態(tài)小于 500 但不等于 401 ); } return false; // 如果不符合條件,則不重試 }; // 配置 axios 實(shí)例的重試機(jī)制 axiosRetry(instance, { retries: 3, // 設(shè)置最多重試次數(shù)為 3 次 retryCondition: customRetryCondition, // 使用自定義的重試條件 retryDelay: axiosRetry.exponentialDelay, // 使用指數(shù)退避算法設(shè)置重試延遲 });
(2)axios添加請(qǐng)求攔截器
// api.jsx // 請(qǐng)求攔截器 instance.interceptors.request.use( async function (config) { console.log('開始請(qǐng)求'); // 打印請(qǐng)求開始信息 const accessToken = localStorage.getItem('accessToken'); // 從 localStorage 獲取 accessToken const refreshToken = localStorage.getItem('refreshToken'); // 從 localStorage 獲取 refreshToken console.log(accessToken); console.log(refreshToken); config.headers['accessToken'] = accessToken // 設(shè)置請(qǐng)求頭中的 accessToken config.headers['refreshToken'] = refreshToken // 設(shè)置請(qǐng)求頭中的 refreshToken return config; // 返回配置 }, function (error) { return Promise.reject(error); // 拒絕請(qǐng)求錯(cuò)誤 } );
(3)axios添加響應(yīng)攔截器
// api.jsx /** * 響應(yīng)攔截器 */ instance.interceptors.response.use( async function (response) { // alert(222) if (response.status === 200) { return response; // 如果響應(yīng)狀態(tài)是 200,返回響應(yīng) } else { return Promise.reject(response.data.message || '未知錯(cuò)誤'); // 否則拒絕并返回錯(cuò)誤信息 } }, function (error) { if (error && error.response) { // 如果有響應(yīng)錯(cuò)誤 switch (error.response.status) { case 400: error.message = '錯(cuò)誤請(qǐng)求'; // 處理 400 錯(cuò)誤 break; case 401: error.message = '未授權(quán),請(qǐng)重新登錄'; // 處理 401 錯(cuò)誤 break; case 403: error.message = '拒絕訪問'; // 處理 403 錯(cuò)誤 localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken // Router.push('/login'); // 跳轉(zhuǎn)到登錄頁(yè)面 window.location.href='/login' break; case 404: error.message = '請(qǐng)求錯(cuò)誤,未找到該資源'; // 處理 404 錯(cuò)誤 break; case 405: error.message = '請(qǐng)求方法未允許'; // 處理 405 錯(cuò)誤 break; case 408: error.message = '請(qǐng)求超時(shí)'; // 處理 408 錯(cuò)誤 break; case 500: error.message = '服務(wù)器端出錯(cuò)'; // 處理 500 錯(cuò)誤 break; case 501: error.message = '網(wǎng)絡(luò)未實(shí)現(xiàn)'; // 處理 501 錯(cuò)誤 break; case 502: error.message = '網(wǎng)絡(luò)錯(cuò)誤'; // 處理 502 錯(cuò)誤 break; case 503: error.message = '服務(wù)不可用'; // 處理 503 錯(cuò)誤 break; case 504: error.message = '網(wǎng)絡(luò)超時(shí)'; // 處理 504 錯(cuò)誤 break; case 505: error.message = 'http版本不支持該請(qǐng)求'; // 處理 505 錯(cuò)誤 break; default: error.message = `連接錯(cuò)誤${error.response.status}`; // 處理其他未知錯(cuò)誤 } } else { error.message = '連接服務(wù)器失敗'; // 如果沒有響應(yīng),打印連接失敗信息 } return Promise.reject(error.message); // 拒絕并返回錯(cuò)誤信息 } );
(4)刷新token
// api.jsx // 重新刷新token async function refresh() { let res =await instance.get('/refresh'); // 發(fā)送請(qǐng)求以刷新 token localStorage.setItem('accessToken', res.data.accessToken); // 將新的 accessToken 存儲(chǔ)到 localStorage }
七、后端完整代碼
(1)app.js
// app.js var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var jwt = require("jsonwebtoken"); var app = express(); // cors var cors = require("cors") app.use(cors()) // 自定義中間件 用于處理jwt驗(yàn)證 app.use((req, res, next) => { // 定義不需要驗(yàn)證的路徑 let pathArr = [ '/userLogin', '/refresh', '/sendYzm', '/resetPassword', '/findUser', '/upload' ] // 如果請(qǐng)求路徑在不需要驗(yàn)證的路徑中,直接調(diào)用next()繼續(xù)處理 if (pathArr.includes(req.path)) { return next() } // 獲取請(qǐng)求頭中的accessToken和refreshToken const accessToken = req.headers.accesstoken const refreshToken = req.headers.refreshtoken // 判斷refreshToken是否過期 try { jwt.verify(refreshToken, "MTHGH") } catch (error) { console.log(error, 111); return res.status(403).send({ message: 'refreshToken驗(yàn)證失敗' }) } // 如果沒有accessToken 返回401 if (!accessToken) { return res.status(401).send({ message: '未獲取到accessToken' }) } // 使用中間件 // app.use('/index',validateRefreshToken,validateAccessToken) // app.use('/user',validateRefreshToken,validateAccessToken) // 驗(yàn)證accessToken try { const user = jwt.verify(accessToken, "MTHGH") res.locals.user = user//將用戶信息存儲(chǔ)在res.locals中,供后續(xù)中間件使用 return next() } catch (error) { return res.status(401).send({ message: 'accessToken驗(yàn)證失敗' }) } }) // token // var expressJWT = require("express-jwt") // app.use(expressJWT({ // secret: "123", // algorithms: ["HS256"] // }).unless({ // path: ["/login", "/upload", {url: /^\/upload/, methods: ['GET']}] // })) // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/upload', express.static(path.join(__dirname, 'upload'))); app.use('/', indexRouter); app.use('/users', usersRouter); // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;
(2)index.js
// index.js var express = require('express'); var router = express.Router(); const jwt = require('jsonwebtoken'); var { userModel } = require("../model/model") const app = express(); app.use(express.json()); // 解析請(qǐng)求中的 JSON 數(shù)據(jù) // 訪問令牌有效期 const ACCESS_TOKEN_EXPIRATION='1h' // 刷新令牌有效期 const REFRESH_TOKEN_EXPIRATION='1d' const SECRET_KEY="MTHGH" const refreshTokenMap=new Map() // 生成函數(shù)令牌 function generateToken(name,expiration){ return jwt.sign({name},SECRET_KEY,{expiresIn:expiration}) } // 封裝生成長(zhǎng)token和短token function getToken(name){ let accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) let refreshToken = generateToken(name,REFRESH_TOKEN_EXPIRATION) const refreshTokens = refreshTokenMap.get(name)||[] refreshTokens.push(refreshToken) refreshTokenMap.set(name,refreshTokens) return { accessToken, refreshToken } } // 賬號(hào)密碼登錄 router.post('/userLogin', async (req, res) => { const {username,password} = req.body let user = await userModel.findOne({userName:username}) if(!user){ return res.status(200).send({message:"賬號(hào)錯(cuò)誤",code:1}) } if(user.passWord!==password){ return res.status(200).send({message:"密碼錯(cuò)誤",code:2}) } let {accessToken,refreshToken}=getToken(user.userName) res.status(200).send({ data:user, accessToken, refreshToken, message:'登陸成功', code:200 }) }) // 刷新短token router.get('/refresh',async(req,res)=>{ const refreshToken = req.headers.refreshtoken // console.log(111); if(!refreshToken){ res.status(403).send("沒有短token") } try{ const {name} = jwt.verify(refreshToken,SECRET_KEY) const accessToken = generateToken(name,ACCESS_TOKEN_EXPIRATION) res.status(200).send({accessToken}) }catch(error){ console.log('長(zhǎng)token已經(jīng)過期'); res.status(403).send('長(zhǎng)token已經(jīng)過期') } }) module.exports = router;
八、前端完整代碼
(1)api.jsx
// api.jsx import axios from "axios"; import axiosRetry from 'axios-retry'; // 基礎(chǔ)配置 const instance = axios.create({ baseURL: "http://localhost:3010", timeout: 5000, headers: { 'Content-Type': 'application/json' }, }); /** * 重試機(jī)制 */ let retryCount = 0; // 初始化重試計(jì)數(shù) const customRetryCondition = async (error) => { // 自定義重試條件 if (axios.isAxiosError(error) && error.response?.status !== 200) { // 如果是 Axios 錯(cuò)誤且響應(yīng)狀態(tài)不是 200 if (error.response?.status === 403) { // 如果后端返回 403(禁止訪問) localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken console.log('請(qǐng)重新登錄'); // 打印提示信息 window.location.href='/login' // 跳轉(zhuǎn)到登錄頁(yè)面 return false; // 不重試 } if (error.response?.status === 401) { // 如果后端返回 401(未授權(quán)) await refresh(); // 嘗試刷新 token console.log('刷新token'); // 打印提示信息 return true; // 允許重試 } retryCount++; // 增加重試計(jì)數(shù) console.log(`第${retryCount}次重試`); // 打印當(dāng)前重試次數(shù) return ( error.response.status >= 500 || // 如果響應(yīng)狀態(tài)是 500 或以上 (error.response.status < 500 && error.response?.status !== 401) // 或者狀態(tài)小于 500 但不等于 401 ); } return false; // 如果不符合條件,則不重試 }; // 配置 axios 實(shí)例的重試機(jī)制 axiosRetry(instance, { retries: 3, // 設(shè)置最多重試次數(shù)為 3 次 retryCondition: customRetryCondition, // 使用自定義的重試條件 retryDelay: axiosRetry.exponentialDelay, // 使用指數(shù)退避算法設(shè)置重試延遲 }); // 請(qǐng)求攔截器 instance.interceptors.request.use( async function (config) { console.log('開始請(qǐng)求'); // 打印請(qǐng)求開始信息 const accessToken = localStorage.getItem('accessToken'); // 從 localStorage 獲取 accessToken const refreshToken = localStorage.getItem('refreshToken'); // 從 localStorage 獲取 refreshToken console.log(accessToken); console.log(refreshToken); config.headers['accessToken'] = accessToken // 設(shè)置請(qǐng)求頭中的 accessToken config.headers['refreshToken'] = refreshToken // 設(shè)置請(qǐng)求頭中的 refreshToken return config; // 返回配置 }, function (error) { return Promise.reject(error); // 拒絕請(qǐng)求錯(cuò)誤 } ); /** * 響應(yīng)攔截器 */ instance.interceptors.response.use( async function (response) { // alert(222) if (response.status === 200) { return response; // 如果響應(yīng)狀態(tài)是 200,返回響應(yīng) } else { return Promise.reject(response.data.message || '未知錯(cuò)誤'); // 否則拒絕并返回錯(cuò)誤信息 } }, function (error) { if (error && error.response) { // 如果有響應(yīng)錯(cuò)誤 switch (error.response.status) { case 400: error.message = '錯(cuò)誤請(qǐng)求'; // 處理 400 錯(cuò)誤 break; case 401: error.message = '未授權(quán),請(qǐng)重新登錄'; // 處理 401 錯(cuò)誤 break; case 403: error.message = '拒絕訪問'; // 處理 403 錯(cuò)誤 localStorage.removeItem('accessToken'); // 移除 accessToken localStorage.removeItem('refreshToken'); // 移除 refreshToken // Router.push('/login'); // 跳轉(zhuǎn)到登錄頁(yè)面 window.location.href='/login' break; case 404: error.message = '請(qǐng)求錯(cuò)誤,未找到該資源'; // 處理 404 錯(cuò)誤 break; case 405: error.message = '請(qǐng)求方法未允許'; // 處理 405 錯(cuò)誤 break; case 408: error.message = '請(qǐng)求超時(shí)'; // 處理 408 錯(cuò)誤 break; case 500: error.message = '服務(wù)器端出錯(cuò)'; // 處理 500 錯(cuò)誤 break; case 501: error.message = '網(wǎng)絡(luò)未實(shí)現(xiàn)'; // 處理 501 錯(cuò)誤 break; case 502: error.message = '網(wǎng)絡(luò)錯(cuò)誤'; // 處理 502 錯(cuò)誤 break; case 503: error.message = '服務(wù)不可用'; // 處理 503 錯(cuò)誤 break; case 504: error.message = '網(wǎng)絡(luò)超時(shí)'; // 處理 504 錯(cuò)誤 break; case 505: error.message = 'http版本不支持該請(qǐng)求'; // 處理 505 錯(cuò)誤 break; default: error.message = `連接錯(cuò)誤${error.response.status}`; // 處理其他未知錯(cuò)誤 } } else { error.message = '連接服務(wù)器失敗'; // 如果沒有響應(yīng),打印連接失敗信息 } return Promise.reject(error.message); // 拒絕并返回錯(cuò)誤信息 } ); // 重新刷新token async function refresh() { let res =await instance.get('/refresh'); // 發(fā)送請(qǐng)求以刷新 token localStorage.setItem('accessToken', res.data.accessToken); // 將新的 accessToken 存儲(chǔ)到 localStorage } // 導(dǎo)出封裝后的 axios 實(shí)例 export default instance;
綜上所述,雙token無感刷新是一種有效的安全及用戶體驗(yàn)優(yōu)化技術(shù),通過合理的Token管理和自動(dòng)刷新機(jī)制,實(shí)現(xiàn)了在不影響用戶體驗(yàn)的前提下提升系統(tǒng)安全性的目標(biāo)。
總結(jié)
到此這篇關(guān)于雙token無感刷新nodejs+React的文章就介紹到這了,更多相關(guān)雙token無感刷新nodejs+React內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node.js+Socket.io實(shí)現(xiàn)雙人在線五子棋對(duì)戰(zhàn)
這篇文章主要為大家詳細(xì)介紹了Node.js+Socket.io實(shí)現(xiàn)雙人在線五子棋對(duì)戰(zhàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05node全局變量__dirname與__filename的區(qū)別
這篇文章主要介紹了node全局變量__dirname與__filename的區(qū)別,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01node-red教程之dashboard簡(jiǎn)介與輸入型儀表板控件的使用
Node-red支持自定義節(jié)點(diǎn),當(dāng)然也就支持自定義圖形化的節(jié)點(diǎn)。也有優(yōu)秀的開發(fā)者把自己建立的圖形化節(jié)點(diǎn)無償分享。這里給出一個(gè)股票界面的例子,讓大家看一看優(yōu)秀的node-red界面能做到什么樣子2022-01-01Node升級(jí)后vue項(xiàng)目node-sass報(bào)錯(cuò)問題及解決
這篇文章主要介紹了Node升級(jí)后vue項(xiàng)目node-sass報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03nodejs 使用nodejs-websocket模塊實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)實(shí)時(shí)通訊
這篇文章主要介紹了nodejs 使用nodejs-websocket模塊實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)實(shí)時(shí)通訊的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-11-11Node.js 使用 gRPC從定義到實(shí)現(xiàn)過程詳解
gRPC是一個(gè)高性能、開源的遠(yuǎn)程過程調(diào)用(RPC)框架,由 Google 開發(fā),它支持多種編程語(yǔ)言,旨在簡(jiǎn)化和優(yōu)化分布式系統(tǒng)中的服務(wù)通信,本文給大家介紹Node.js 使用 gRPC從定義到實(shí)現(xiàn)過程,感興趣的朋友跟隨小編一起看看吧2024-07-07詳解通過源碼解析Node.js中cluster模塊的主要功能實(shí)現(xiàn)
這篇文章主要介紹了詳解通過源碼解析Node.js中cluster模塊的主要功能實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05