js實(shí)現(xiàn)無感刷新的實(shí)踐(附前后端實(shí)現(xiàn))
無感刷新的核心思路:

無感刷新機(jī)制的目的是在用戶不知情的情況下,自動更新其認(rèn)證令牌(通常是Access Token),以保證用戶的會話不會中斷。這通常涉及到兩種類型的令牌:
Access Token:它是用戶進(jìn)行認(rèn)證后得到的令牌,允許用戶訪問服務(wù)器的受保護(hù)資源。它有一個(gè)較短的有效期。
Refresh Token:它是在同一時(shí)間發(fā)放給用戶的另一個(gè)令牌,用于在Access Token過期時(shí)獲取一個(gè)新的Access Token。它的有效期比Access Token長。
當(dāng)Access Token即將過期或已經(jīng)過期時(shí),客戶端會使用Refresh Token向認(rèn)證服務(wù)器請求一個(gè)新的Access Token。如果Refresh Token仍然有效,認(rèn)證服務(wù)器則發(fā)放一個(gè)新的Access Token給客戶端,并且可能會同時(shí)發(fā)放一個(gè)新的Refresh Token。這個(gè)過程對用戶來說是沒有感知的,因此被稱為“無感”刷新。
在項(xiàng)目中實(shí)施無感刷新:
后端實(shí)施步驟:
認(rèn)證端點(diǎn)設(shè)置:
- 設(shè)計(jì)一個(gè)認(rèn)證API端點(diǎn),當(dāng)用戶初次登錄時(shí),返回Access Token和Refresh Token。
- 設(shè)計(jì)一個(gè)Token刷新API端點(diǎn),只接受Refresh Token并返回新的Access Token(可選地返回新的Refresh Token)。
Token管理:
- Access Token應(yīng)有一個(gè)短暫的生命周期,例如15分鐘。
- Refresh Token應(yīng)有一個(gè)長期的生命周期,例如7天或更長,且應(yīng)該存儲在一個(gè)安全的存儲中。
安全考慮:
- 對Refresh Token進(jìn)行旋轉(zhuǎn)(每次使用后就廢棄舊的Refresh Token并發(fā)放一個(gè)新的)。
- 通過HTTPS交換所有Token。
- 應(yīng)用適當(dāng)?shù)募用艽胧﹣肀Wo(hù)Token的安全。
前端實(shí)施步驟:
存儲Token:
- 在客戶端安全地存儲Access Token和Refresh Token(例如使用Web的localStorage或SecureStorage)。
攔截請求和響應(yīng):
- 使用攔截器監(jiān)視所有出站請求和進(jìn)站響應(yīng)。
- 在請求頭中自動加入Access Token。
處理過期的Access Token:
- 當(dāng)接收到表示Token過期的HTTP狀態(tài)碼(例如401)時(shí),暫停發(fā)出的請求。
- 使用Refresh Token請求新的Access Token。
處理新的Access Token:
- 更新存儲中的Access Token。
- 重新發(fā)送之前因Token過期而暫停的請求。
處理Refresh Token過期:
- 如果Refresh Token也過期或無效,引導(dǎo)用戶重新登錄。
無感刷新機(jī)制的大概思路就是這些,下面是具體的示例,分為簡化版和完整版,簡化版目的是更好的了解無感刷新的原理,而完整版就要考慮一些其他問題,比如說安全問題。
下面是實(shí)現(xiàn)無感刷新機(jī)制的具體示例(簡化版)
這個(gè)例子涵蓋前端(使用JavaScript)和后端(Node.js環(huán)境下使用Express框架)
后端(Node.js/Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const accessTokenSecret = 'YOUR_ACCESS_TOKEN_SECRET';
const refreshTokenSecret = 'YOUR_REFRESH_TOKEN_SECRET';
let refreshTokens = [];
app.post('/login', (req, res) => {
// 用戶登錄邏輯,驗(yàn)證用戶憑證
const { username, password } = req.body;
// 這里應(yīng)該有邏輯來驗(yàn)證用戶憑證
// 如果驗(yàn)證成功:
const accessToken = jwt.sign({ username }, accessTokenSecret, { expiresIn: '15m' });
const refreshToken = jwt.sign({ username }, refreshTokenSecret);
refreshTokens.push(refreshToken);
res.json({
accessToken,
refreshToken
});
});
app.post('/refresh', (req, res) => {
// 用戶發(fā)送refresh token來獲取新的access token
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.sendStatus(403);
}
jwt.verify(refreshToken, refreshTokenSecret, (err, user) => {
if (err) {
return res.sendStatus(403);
}
const newAccessToken = jwt.sign({ username: user.username }, accessTokenSecret, { expiresIn: '15m' });
res.json({
accessToken: newAccessToken
});
});
});
app.listen(3000, () => {
console.log('Authentication service started on port 3000');
});
前端(JavaScript/AJAX)
假設(shè)使用了axios作為HTTP客戶端,可以設(shè)置攔截器來自動處理Token刷新。
axios.interceptors.response.use(response => {
return response;
}, error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
return axios.post('/refresh', {
refreshToken: localStorage.getItem('refreshToken')
}).then(res => {
if (res.status === 200) {
localStorage.setItem('accessToken', res.data.accessToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('accessToken');
return axios(originalRequest);
}
});
}
return Promise.reject(error);
});
這段代碼中,我們在響應(yīng)攔截器中檢查任何錯誤的響應(yīng)。如果我們得到一個(gè)401響應(yīng),并且這是第一次重試,我們就發(fā)送一個(gè)帶有refreshToken的請求到/refresh端點(diǎn)。如果刷新Token成功,我們就保存新的accessToken,更新請求頭,并重新發(fā)起失敗的請求。
完整示例
實(shí)現(xiàn)非簡化的登錄無感刷新機(jī)制通常會涉及更詳細(xì)的認(rèn)證流程、錯誤處理、日志記錄和安全性措施。這包括使用數(shù)據(jù)庫存儲Refresh Tokens、自動撤銷機(jī)制、雙因素認(rèn)證等。
后端實(shí)現(xiàn)(Node.js/Express + MongoDB)
首先,你需要設(shè)置一個(gè)數(shù)據(jù)庫來存儲Refresh Tokens。出于安全考慮,每個(gè)Refresh Token都應(yīng)該與一個(gè)用戶賬戶關(guān)聯(lián),并且能夠被追蹤和撤銷。
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('./models/User'); // 導(dǎo)入U(xiǎn)ser模型
const RefreshToken = require('./models/RefreshToken'); // 導(dǎo)入RefreshToken模型
const app = express();
// ...其他必要的中間件和數(shù)據(jù)庫連接代碼...
app.post('/login', async (req, res) => {
// 用戶登錄邏輯,驗(yàn)證用戶憑證
const { username, password } = req.body;
const user = await User.findOne({ username });
if (user && bcrypt.compareSync(password, user.password)) {
const accessToken = jwt.sign({ userId: user._id }, accessTokenSecret, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user._id }, refreshTokenSecret);
// 保存refresh token到數(shù)據(jù)庫
const newRefreshToken = new RefreshToken({ token: refreshToken, user: user._id });
await newRefreshToken.save();
res.json({ accessToken, refreshToken });
} else {
res.status(401).send('Username or password incorrect');
}
});
app.post('/refresh', async (req, res) => {
// 用戶發(fā)送refresh token來獲取新的access token
const { refreshToken } = req.body;
const dbToken = await RefreshToken.findOne({ token: refreshToken });
if (!dbToken) {
return res.status(403).send('Refresh token not found');
}
jwt.verify(refreshToken, refreshTokenSecret, async (err, decoded) => {
if (err) {
return res.status(403).send('Refresh token invalid');
}
// 生成新的access token和refresh token
const newAccessToken = jwt.sign({ userId: decoded.userId }, accessTokenSecret, { expiresIn: '15m' });
const newRefreshToken = jwt.sign({ userId: decoded.userId }, refreshTokenSecret);
// 更新數(shù)據(jù)庫中的refresh token
dbToken.token = newRefreshToken;
await dbToken.save();
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
});
});
app.listen(3000, () => {
console.log('Authentication service started on port 3000');
});
在此示例中,我們使用了MongoDB來存儲用戶和他們的Refresh Tokens。登錄時(shí),我們驗(yàn)證用戶憑證,并且如果認(rèn)證成功,我們就生成Access Token和Refresh Token,然后將Refresh Token保存到數(shù)據(jù)庫。在無感刷新流程中,我們驗(yàn)證提供的Refresh Token是否存在于數(shù)據(jù)庫中,并且是否有效,然后發(fā)放新的Access Token。
前端實(shí)現(xiàn)(JavaScript/AJAX + Local Storage)
在前端實(shí)現(xiàn)中,我們需要確保存儲Token的方法是安全的。在生產(chǎn)環(huán)境中,你可能需要考慮使用更安全的存儲方式,如HTTPOnly Cookies或Secure Local Storage。
axios.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
// 檢測token過期的錯誤代碼,比如401
if (error.response.status === 401 && originalRequest.url === '/refresh') {
// 刷新Token失敗,直接登出用戶
logoutUser();
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
return axios.post('/refresh', { refreshToken: localStorage.getItem('refreshToken') })
.then(res => {
if (res.status === 200) {
// 將新token設(shè)置到本地存儲和axios默認(rèn)頭部
localStorage.setItem('accessToken', res.data.accessToken);
localStorage.setItem('refreshToken', res.data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + res.data.accessToken;
// 更新失敗請求的頭部并重新發(fā)送
originalRequest.headers['Authorization'] = 'Bearer ' + res.data.accessToken;
return axios(originalRequest);
}
})
.catch(error => {
// 任何錯誤都直接登出用戶
logoutUser();
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
function logoutUser() {
// 清除本地存儲和狀態(tài),重定向到登錄頁面
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// ...重定向到登錄頁的代碼...
}
在前端的實(shí)現(xiàn)中,我們創(chuàng)建了一個(gè)axios攔截器,它會在遇到401未授權(quán)的響應(yīng)時(shí)自動嘗試刷新Token。如果刷新Token請求也失敗,則觸發(fā)用戶登出的邏輯。
注意:此功能一定要注意其安全性,具體的話要結(jié)合實(shí)際的項(xiàng)目,以下是我的一些建議:
安全方面通常包括以下幾個(gè)方法:
1. 使用HTTPS來加密所有的通信。
2. 安全地存儲Access Token和Refresh Token,如使用httpOnly和Secure屬性的Cookies。
3. 對Token進(jìn)行定期輪換,特別是Refresh Token。
4. 在服務(wù)器端驗(yàn)證Token的簽名。
5. 設(shè)置適當(dāng)?shù)腡oken過期時(shí)間。
6. 實(shí)現(xiàn)Token撤銷邏輯,以便在檢測到異常時(shí)能夠立即廢棄使用。
7. 避免在客戶端暴露敏感的認(rèn)證邏輯。
8. 對所有的認(rèn)證請求和Token刷新請求進(jìn)行率限制和異常監(jiān)測。
9. 使用跨站請求偽造(CSRF)保護(hù)措施。
10. 確??蛻舳撕头?wù)端都有充分的錯誤處理和日志記錄機(jī)制。
到此這篇關(guān)于js實(shí)現(xiàn)無感刷新的實(shí)踐(附前后端實(shí)現(xiàn))的文章就介紹到這了,更多相關(guān)js 無感刷新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 如何實(shí)現(xiàn)無感刷新token
- 詳細(xì)聊聊前端如何實(shí)現(xiàn)token無感刷新(refresh_token)
- Vue3+Vite使用雙token實(shí)現(xiàn)無感刷新
- 前端使用vue實(shí)現(xiàn)token無感刷新的三種方案解析
- js項(xiàng)目中前端如何實(shí)現(xiàn)無感刷新token
- Vue項(xiàng)目實(shí)現(xiàn)token無感刷新的示例代碼
- VUE前端實(shí)現(xiàn)token的無感刷新3種方案(refresh_token)
- VUE前端實(shí)現(xiàn)token的無感刷新方式
- vue2實(shí)現(xiàn)無感刷新token的方式詳解
- Vue實(shí)現(xiàn)雙token無感刷新的示例代碼
相關(guān)文章
js復(fù)制網(wǎng)頁內(nèi)容并兼容各主流瀏覽器的代碼
js 復(fù)制網(wǎng)頁內(nèi)容的方法代碼有很多不過要兼容各瀏覽器就不多了,下面有個(gè)不錯的方法,大家可以嘗試操作下2013-12-12
前端無法獲取響應(yīng)頭(如Content-Disposition)的原因與解決方案詳析
Content-Disposition 是 HTTP 協(xié)議中的一個(gè)頭部字段,用于指示文件的名稱和下載方式,這篇文章主要介紹了前端無法獲取響應(yīng)頭(如Content-Disposition)的原因與解決方案的相關(guān)資料,需要的朋友可以參考下2026-02-02
JavaScript獲取/更改文本框的值的實(shí)例代碼
這篇文章介紹了JavaScript獲取/更改文本框的值的實(shí)例代碼,有需要的朋友可以參考一下2013-08-08
JS+HTML實(shí)現(xiàn)自定義上傳圖片按鈕并顯示圖片功能的方法分析
這篇文章主要介紹了JS+HTML實(shí)現(xiàn)自定義上傳圖片按鈕并顯示圖片功能的方法,結(jié)合實(shí)例形式分析了JavaScript圖片上傳、編碼轉(zhuǎn)換等相關(guān)操作技巧,需要的朋友可以參考下2020-02-02
js添加table的行和列 具體實(shí)現(xiàn)方法
這篇文章介紹了js添加table的行和列 具體實(shí)現(xiàn)方法,有需要的朋友可以參考一下2013-07-07
原生JS封裝animate運(yùn)動框架的實(shí)例
下面小編就為大家?guī)硪黄鶭S封裝animate運(yùn)動框架的實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10
JavaScript利用時(shí)間分片實(shí)現(xiàn)高性能渲染數(shù)據(jù)詳解
為了豐富我們的知識體系,我們有必要了解并清楚當(dāng)遇到大量數(shù)據(jù)時(shí),如何才能在不卡主頁面的情況下渲染數(shù)據(jù),以及其中背后的原理,本文介紹了如何使用時(shí)間分片的方式來渲染大量數(shù)據(jù),感興趣的可以了解下2023-05-05

