node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無感刷新機(jī)制的示例
為什么要實(shí)施雙token機(jī)制?
| 優(yōu)點(diǎn) | 描述 |
|---|---|
| 安全性 | Access Token 短期有效,降低泄露風(fēng)險(xiǎn);Refresh Token 權(quán)限受限,僅用于獲取新 Token |
| 用戶體驗(yàn) | 用戶無需頻繁重新登錄,Token 自動(dòng)刷新過程對(duì)用戶透明 |
| 靈活性 | 獨(dú)立控制不同 Token 的生命周期,適應(yīng)各種場(chǎng)景需求 |
| 可管理性 | 支持多設(shè)備登錄管理,便于撤銷特定設(shè)備的登錄狀態(tài) |
| 性能優(yōu)化 | 減少數(shù)據(jù)庫(kù)查詢次數(shù),提升系統(tǒng)響應(yīng)速度 |
實(shí)現(xiàn)方案:
| 模塊 | 實(shí)現(xiàn)方式 |
|---|---|
| 登錄接口 | 返回 accessToken 和 refreshToken,分別存入 Cookie |
| Access Token | 短時(shí)效 JWT,用于請(qǐng)求鑒權(quán) |
| Refresh Token | 長(zhǎng)時(shí)效 JWT,用于刷新 Access Token |
| Token 校驗(yàn)方式 | 后端從 Cookie 中讀取 token(即 Access Token) |
| 前端 Axios | 使用響應(yīng)攔截器統(tǒng)一處理 Token 失效和自動(dòng)刷新 |
- 使用 JWT 生成兩個(gè) Token:
- Access Token(短時(shí)效):用于接口認(rèn)證,例如有效期為 15 分鐘
- Refresh Token(長(zhǎng)時(shí)效):用于刷新 Access Token,例如有效期為 7 天
- 在用戶登錄時(shí)返回這兩個(gè) Token,并將 Refresh Token 存儲(chǔ)在數(shù)據(jù)庫(kù)中
- 當(dāng) Access Token 過期后,客戶端使用 Refresh Token 請(qǐng)求新的 Access Token
- 如果 Refresh Token 也過期或無效,則強(qiáng)制重新登錄
具體代碼實(shí)現(xiàn)
1. 安裝依賴:
cookie-parser用來解析 Cookie 中的 Token
npm install jsonwebtoken bcryptjs cookie-parser
2. 數(shù)據(jù)庫(kù)添加兩個(gè)字段
| refresh_token | VARCHAR(255) | 加密后的 RefreshToken |
|---|---|---|
| expires_at | DATETIME | RefreshToken 過期時(shí)間 |
3. 在后端cors跨域中間中添加屬性
// 將cors注冊(cè)為全局中間件
app.use(cors({
origin: 'http://localhost:5173', // 前端地址
credentials: true // ?? 允許攜帶憑證(cookies)
}))
3. 登錄邏輯改造(添加雙token)
- 添加配置文件config.js
module.exports = {
jwtSecretKey: 'yke;eky1]239_jwt87-2up34',
refreshTokenSecretKey: 'yke;eky1]239_refresh87-2up34',
accessExpiresIn: '15m', // 訪問令牌有效期
refreshExpiresIn: '7d', // 刷新令牌有效期
accessExpiresInSec: 15 * 60, // 秒數(shù)
refreshExpiresInSec: 7 * 24 * 60 * 60 // 秒數(shù)
}
- 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 }
)
- 生成token過期時(shí)間,和refreshToken一起存入數(shù)據(jù)庫(kù)
const expiresAt = new Date() expiresAt.setSeconds(expiresAt.getSeconds() + config.refreshExpiresInSec)
- 將accessToken訪問token、refreshToken刷新token存入cookie
// 設(shè)置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'
})
登錄邏輯完整代碼:
// 用戶登錄的處理函數(shù)
exports.login = (req, res) => {
// 接收表單數(shù)據(jù)
const userInfo = req.body
console.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 })
}
// 執(zhí)行sql語句成功,但是獲取的條數(shù)不等于1
if (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: '密碼錯(cuò)誤' })
}
// 在服務(wù)器端生成Token字符串
const user = { ...results[0] }
// 生成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 }
)
// 將refresh token存儲(chǔ)到數(shù)據(jù)庫(kù)中
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失敗' })
}
// 設(shè)置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'
})
res.send({
status: 0,
message: '登錄成功',
data: {
username: results[0].username
}
})
})
})
}
4. 實(shí)現(xiàn)token刷新接口
創(chuàng)建新路由/refreshToken
// token刷新接口
exports.refreshToken = (req, res) => {
// 直接從cookie中獲取刷新token => 前端不需要再單獨(dú)把token傳入請(qǐng)求頭
const refreshToken = req.cookies.refresh_token
// 判斷refresh token是否存在
if (!refreshToken) {
return res.send({ status: 1, message: '缺少refreshToken,請(qǐng)先登錄' })
}
try {
// 驗(yàn)證refreshToken
const 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 token
const accessToken = jwt.sign(
{ id: user.id, username: user.username, email: user.email },
config.jwtSecretKey,
{ expiresIn: config.accessExpiresIn }
)
// 更新accessToken到Cookie
res.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已過期,請(qǐng)重新登錄' })
}
}
5. 響應(yīng)攔截器中處理token
import axios from 'axios'
import { message } from 'antd'
import { refreshTokenService } from '@/api/user'
const instance = axios.create({
baseURL: 'http://localhost:3333', // 你的API服務(wù)器地址
timeout: 10000, // 請(qǐng)求超時(shí)時(shí)間
headers: {
'Content-Type': 'application/json'
},
// 必須加上這個(gè)選項(xiàng)才能跨域攜帶
withCredentials: true
})
// 添加請(qǐng)求攔截器
instance.interceptors.request.use(
(config) => {
// 后端將token存在了cookie中,這里不需要攜帶token
return config
},
(err) => Promise.reject(err)
)
// 標(biāo)記是否正在刷新 Token(防止并發(fā)刷新)
let isRefreshing = false
// 保存所有因 Token 失效而等待新 Token 的請(qǐng)求回調(diào)函數(shù)
let refreshSubscribers = []
// 成功獲取到新的 Token 后,執(zhí)行所有等待的請(qǐng)求
function onRefreshed(newToken) {
refreshSubscribers.forEach((cb) => cb(newToken))
refreshSubscribers = []
}
// 將等待刷新 Token 的請(qǐng)求封裝成一個(gè)回調(diào)函數(shù),加入隊(duì)列中
function addRefreshSubscriber(callback) {
refreshSubscribers.push(callback)
}
// 響應(yīng)攔截器
instance.interceptors.response.use(
(res) => {
console.log(res)
// 摘取核心響應(yīng)數(shù)據(jù)
if (res.data.status === 0) {
return res
}
// 處理業(yè)務(wù)失敗
message.error({type: 'error', content: res.data.message || '服務(wù)異常'})
return Promise.reject(res.data)
},
async (err) => {
// 錯(cuò)誤的特殊情況 => 401權(quán)限不足或token過期 => 攔截到登錄
const originalRequest = err.config
// 判斷是否是 401 并且不是已經(jīng)重試過的請(qǐng)求
if (err.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
// 控制 Token 刷新流程(防止多次刷新)
if (!isRefreshing) {
// 標(biāo)記刷新狀態(tài)
isRefreshing = true
try {
const res = await refreshTokenService()
const newToken = res.data.data.token
// 重試請(qǐng)求
onRefreshed(newToken)
} catch {
// 刷新失敗
message.error({ type: 'error', content: '登錄已過期,請(qǐng)重新登錄' })
// 跳轉(zhuǎn)登錄
if (window.location.pathname !== '/login') {
history.push('/login')
}
} finally {
isRefreshing = false
}
}
// 把當(dāng)前請(qǐng)求放入隊(duì)列,等待 Token 刷新后再重發(fā)
return new Promise((resolve) => {
addRefreshSubscriber((newToken) => {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
resolve(instance(originalRequest))
})
})
} else {
// 錯(cuò)誤的默認(rèn)情況 =》 只給提示
message.error({ type: 'error', content: err.response.data.message || '服務(wù)異常' })
}
return Promise.reject(err)
}
)
export default instance
到此這篇關(guān)于node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無感刷新機(jī)制的示例的文章就介紹到這了,更多相關(guān)node.js 雙Token+Cookie存儲(chǔ)+無感刷新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 前端登錄token失效實(shí)現(xiàn)雙Token無感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無感刷新
- 雙Token實(shí)現(xiàn)無感刷新的完整代碼示例
- 雙token無感刷新nodejs+React詳細(xì)解釋(保姆級(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)文章
node學(xué)習(xí)筆記之讀寫文件與開啟第一個(gè)web服務(wù)器操作示例
這篇文章主要介紹了node學(xué)習(xí)筆記之讀寫文件與開啟第一個(gè)web服務(wù)器操作,結(jié)合實(shí)例形式分析了nodejs文件讀寫及創(chuàng)建web服務(wù)接收request請(qǐng)求與處理相關(guān)操作技巧,需要的朋友可以參考下2019-05-05
Node.js?內(nèi)置模塊fs文件系統(tǒng)操作示例詳解
這篇文章主要為大家介紹了Node.js?內(nèi)置模塊fs文件系統(tǒng)操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
node文件上傳功能簡(jiǎn)易實(shí)現(xiàn)代碼
本篇文章主要介紹了node文件上傳功能簡(jiǎn)易實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06
koa+mongoose實(shí)現(xiàn)簡(jiǎn)單增刪改查接口的示例代碼
這篇文章主要介紹了koa+mongoose實(shí)現(xiàn)簡(jiǎn)單增刪改查接口的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-05-05
Node.js實(shí)現(xiàn)的簡(jiǎn)易網(wǎng)頁抓取功能示例
這篇文章主要介紹了Node.js實(shí)現(xiàn)的簡(jiǎn)易網(wǎng)頁抓取功能示例,本文使用了PhantomJS、node-phantomjs等庫(kù)實(shí)現(xiàn),需要的朋友可以參考下2014-12-12

