雙token無感刷新nodejs+React詳細(xì)解釋(保姆級教程)
雙token無感刷新是一種在Web開發(fā)中常用的安全及用戶體驗(yàn)優(yōu)化技術(shù)。以下是對雙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。有效期通常較長,甚至可以說是永久的,但出于安全考慮,一般會設(shè)置過期時(shí)間或使用次數(shù)限制。Refresh Token通常不會直接發(fā)送給客戶端,而是保存在服務(wù)器端或經(jīng)過加密后存儲在客戶端本地(如localStorage或sessionStorage)。
二、工作原理
雙token無感刷新的核心在于自動在后臺處理Access Token的過期和刷新過程,而無需用戶重新登錄或感知到這一過程。具體流程如下:
- 用戶通過用戶名(賬號)、密碼或其他認(rèn)證方式向認(rèn)證服務(wù)器請求授權(quán)。
- 認(rèn)證成功后,服務(wù)器返回Access Token和Refresh Token給前端。前端將這兩個(gè)Token保存到本地存儲中,以便在需要時(shí)使用。
- 前端在訪問受保護(hù)資源時(shí),將Access Token放入請求頭中發(fā)送給后端。
- 如果Access Token有效,后端正常處理請求并返回結(jié)果。
- 如果Access Token過期,后端會返回一個(gè)錯(cuò)誤響應(yīng)(如HTTP 401 Unauthorized)。
- 前端在接收到錯(cuò)誤響應(yīng)后,自動在后臺使用Refresh Token向認(rèn)證服務(wù)器請求新的Access Token。
- 認(rèn)證服務(wù)器驗(yàn)證Refresh Token的有效性后,返回一個(gè)新的Access Token和(可選的)新的Refresh Token。
- 前端更新本地存儲的Access Token和Refresh Token,并重新發(fā)起之前的請求。
三、實(shí)現(xiàn)方式
雙token無感刷新的實(shí)現(xiàn)通常依賴于前端和后端的配合。以下是一個(gè)簡化的實(shí)現(xiàn)流程:
前端實(shí)現(xiàn):
- 設(shè)置全局請求攔截器,在發(fā)送請求前檢查Access Token的有效性。
- 如果Access Token即將過期或已過期,自動使用Refresh Token請求新的Access Token。
- 更新本地存儲中的Access Token和Refresh Token。
- 重新發(fā)起因Access Token過期而失敗的請求。
后端實(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)勢與應(yīng)用場景
雙token無感刷新的優(yōu)勢在于提高了系統(tǒng)的安全性和用戶體驗(yàn)。通過分離短期和長期的憑證,降低了Access Token被泄露的風(fēng)險(xiǎn),同時(shí)避免了用戶因Token過期而頻繁重新登錄的麻煩。
這一機(jī)制廣泛應(yīng)用于需要高安全性和良好用戶體驗(yàn)的場景,如Web應(yīng)用和移動應(yī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請求中的Cookie頭
var logger = require('morgan'); //用于將請求日志輸出到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'
]
// 如果請求路徑在不需要驗(yàn)證的路徑中,直接調(diào)用next()繼續(xù)處理
if (pathArr.includes(req.path)) {
return next()
}
// 獲取請求頭中的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//將用戶信息存儲在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請求
const jwt = require('jsonwebtoken'); //實(shí)現(xiàn)用戶身份驗(yàn)證和授權(quán)等功能(4)封裝生成長token和短token的函數(shù)
// index.js
// 訪問令牌有效期
const ACCESS_TOKEN_EXPIRATION='1h'
// 刷新令牌有效期
const REFRESH_TOKEN_EXPIRATION='1d'
// 令牌密鑰
const SECRET_KEY="MTHGH"
// 這里創(chuàng)建了一個(gè)Map對象,用于存儲用戶名和對應(yīng)的刷新令牌列表。這樣,當(dāng)用戶登錄或刷新令牌時(shí),可以跟蹤和驗(yàn)證他們的刷新令牌。
const refreshTokenMap=new Map()
// 生成函數(shù)令牌
function generateToken(name,expiration){
return jwt.sign({name},SECRET_KEY,{expiresIn:expiration})
}
// 封裝生成長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)賬號密碼登錄
// 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:"賬號錯(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)=>{
// 獲取請求頭的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('長token已經(jīng)過期');
res.status(403).send('長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('請重新登錄'); // 打印提示信息
window.location.href='/login' // 跳轉(zhuǎn)到登錄頁面
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添加請求攔截器
// api.jsx
// 請求攔截器
instance.interceptors.request.use(
async function (config) {
console.log('開始請求'); // 打印請求開始信息
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è)置請求頭中的 accessToken
config.headers['refreshToken'] = refreshToken // 設(shè)置請求頭中的 refreshToken
return config; // 返回配置
},
function (error) {
return Promise.reject(error); // 拒絕請求錯(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ò)誤請求'; // 處理 400 錯(cuò)誤
break;
case 401:
error.message = '未授權(quán),請重新登錄'; // 處理 401 錯(cuò)誤
break;
case 403:
error.message = '拒絕訪問'; // 處理 403 錯(cuò)誤
localStorage.removeItem('accessToken'); // 移除 accessToken
localStorage.removeItem('refreshToken'); // 移除 refreshToken
// Router.push('/login'); // 跳轉(zhuǎn)到登錄頁面
window.location.href='/login'
break;
case 404:
error.message = '請求錯(cuò)誤,未找到該資源'; // 處理 404 錯(cuò)誤
break;
case 405:
error.message = '請求方法未允許'; // 處理 405 錯(cuò)誤
break;
case 408:
error.message = '請求超時(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版本不支持該請求'; // 處理 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ā)送請求以刷新 token
localStorage.setItem('accessToken', res.data.accessToken); // 將新的 accessToken 存儲到 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'
]
// 如果請求路徑在不需要驗(yàn)證的路徑中,直接調(diào)用next()繼續(xù)處理
if (pathArr.includes(req.path)) {
return next()
}
// 獲取請求頭中的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//將用戶信息存儲在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()); // 解析請求中的 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})
}
// 封裝生成長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
}
}
// 賬號密碼登錄
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:"賬號錯(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('長token已經(jīng)過期');
res.status(403).send('長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('請重新登錄'); // 打印提示信息
window.location.href='/login' // 跳轉(zhuǎn)到登錄頁面
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è)置重試延遲
});
// 請求攔截器
instance.interceptors.request.use(
async function (config) {
console.log('開始請求'); // 打印請求開始信息
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è)置請求頭中的 accessToken
config.headers['refreshToken'] = refreshToken // 設(shè)置請求頭中的 refreshToken
return config; // 返回配置
},
function (error) {
return Promise.reject(error); // 拒絕請求錯(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ò)誤請求'; // 處理 400 錯(cuò)誤
break;
case 401:
error.message = '未授權(quán),請重新登錄'; // 處理 401 錯(cuò)誤
break;
case 403:
error.message = '拒絕訪問'; // 處理 403 錯(cuò)誤
localStorage.removeItem('accessToken'); // 移除 accessToken
localStorage.removeItem('refreshToken'); // 移除 refreshToken
// Router.push('/login'); // 跳轉(zhuǎn)到登錄頁面
window.location.href='/login'
break;
case 404:
error.message = '請求錯(cuò)誤,未找到該資源'; // 處理 404 錯(cuò)誤
break;
case 405:
error.message = '請求方法未允許'; // 處理 405 錯(cuò)誤
break;
case 408:
error.message = '請求超時(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版本不支持該請求'; // 處理 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ā)送請求以刷新 token
localStorage.setItem('accessToken', res.data.accessToken); // 將新的 accessToken 存儲到 localStorage
}
// 導(dǎo)出封裝后的 axios 實(shí)例
export default instance;綜上所述,雙token無感刷新是一種有效的安全及用戶體驗(yàn)優(yōu)化技術(shù),通過合理的Token管理和自動刷新機(jī)制,實(shí)現(xiàn)了在不影響用戶體驗(yàn)的前提下提升系統(tǒng)安全性的目標(biāo)。
總結(jié)
到此這篇關(guān)于雙token無感刷新nodejs+React的文章就介紹到這了,更多相關(guān)雙token無感刷新nodejs+React內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 前端登錄token失效實(shí)現(xiàn)雙Token無感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無感刷新
- 雙Token實(shí)現(xiàn)無感刷新的完整代碼示例
- node.js實(shí)現(xiàn)雙Token+Cookie存儲+無感刷新機(jī)制的示例
- 雙Token無感刷新機(jī)制實(shí)現(xiàn)方式
- 前端雙token無感刷新圖文詳解
- vue中雙token和無感刷新token的區(qū)別
- Vue實(shí)現(xiàn)雙token無感刷新的示例代碼
- Vue3+Vite使用雙token實(shí)現(xiàn)無感刷新
- SpringBoot+React中雙token實(shí)現(xiàn)無感刷新
相關(guān)文章
nodejs使用express創(chuàng)建一個(gè)簡單web應(yīng)用
這篇文章主要介紹了nodejs使用express創(chuàng)建一個(gè)簡單web應(yīng)用的相關(guān)資料,需要的朋友可以參考下2017-03-03
nodejs與瀏覽器中全局對象區(qū)別點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是一篇關(guān)于nodejs與瀏覽器中全局對象區(qū)別點(diǎn)總結(jié)內(nèi)容,對此有需要的朋友們可以學(xué)習(xí)下。2021-12-12
node.js中的fs.truncateSync方法使用說明
這篇文章主要介紹了node.js中的fs.truncateSync方法使用說明,本文介紹了fs.truncateSync的方法說明、語法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
Node.js中環(huán)境變量process.env的一些事詳解
這篇文章主要給大家介紹了關(guān)于Node.js中環(huán)境變量process.env的一些事,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用node.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
node.js在Linux下執(zhí)行shell命令、.sh腳本的問題
很多時(shí)候需要多個(gè)命令來完成一項(xiàng)工作,而這個(gè)工作又常常是重復(fù)的,這個(gè)時(shí)候我們自然會想到將這些命令寫成sh腳本,下次執(zhí)行下這個(gè)腳本一切就都搞定了,下面就是發(fā)布代碼的一個(gè)腳本示例2022-01-01

