雙Token實(shí)現(xiàn)無(wú)感刷新的完整代碼示例
一、為什么需要無(wú)感刷新?
想象一下你正在刷視頻,突然提示"登錄已過(guò)期,請(qǐng)重新登錄",需要退出當(dāng)前頁(yè)面重新輸入密碼。這樣的體驗(yàn)非常糟糕!無(wú)感刷新就是為了解決這個(gè)問題:讓用戶在不知不覺中完成身份續(xù)期,保持長(zhǎng)時(shí)間在線狀態(tài)。
二、雙Token機(jī)制原理
我們使用兩個(gè)令牌:
- 短令牌:access_token(1小時(shí)):用于日常請(qǐng)求
- 長(zhǎng)令牌:refresh_token(7天):專門用來(lái)刷新令牌
工作流程:
用戶登錄 → 獲取雙令牌 → access_token過(guò)期 → 用refresh_token獲取新的雙令牌 → 自動(dòng)續(xù)期
三、前端實(shí)現(xiàn)(Vue + Axios)
1. 登錄存儲(chǔ)令牌
const login = async () => {
const res = await userLogin(user); //賬號(hào)密碼
// 保存雙令牌到本地
localStorage.setItem('access_token', res.access_token);
localStorage.setItem('refresh_token', res.refresh_token);
}
2. 請(qǐng)求自動(dòng)攜帶令牌
通過(guò)請(qǐng)求攔截器自動(dòng)添加認(rèn)證頭:
api.interceptors.request.use(config => {
const access_token = localStorage.getItem('access_token');
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
})
3. 智能令牌刷新
響應(yīng)攔截器發(fā)現(xiàn)401登錄過(guò)期的錯(cuò)誤時(shí)自動(dòng)請(qǐng)求刷新
驗(yàn)證長(zhǎng)令牌是否失效
- 失效重定向到登錄頁(yè)面
- 未失效重新獲取雙令牌并重新發(fā)起請(qǐng)求
api.interceptors.response.use(
(response) => {
return response
},
async (error) => { // 響應(yīng)失敗
const { data, status, config } = error.response;
if (status === 401 && config.url !== '/refresh') {
// 刷新token
const res = await refreshToken() // 校驗(yàn)的函數(shù)
if (res.status === 200) { // token刷新成功
// 重新將剛剛失敗的請(qǐng)求發(fā)送出去
return api(config)
} else {
// 重定向到登錄頁(yè) router.push('/login')
window.location.href = '/login'
}
}
}
)
四、后端實(shí)現(xiàn)(Node.js + Express)
1. 生成雙令牌
// 生成1小時(shí)有效的access_token const access_token = generateToken(user, '1h'); // 生成7天有效的refresh_token const refresh_token = generateToken(user, '7d');
2. 令牌刷新接口
app.get('/refresh', (req, res) => {
const oldRefreshToken = req.query.token;
try {
// 驗(yàn)證refresh_token有效性
const userData = verifyToken(oldRefreshToken);
// 生成新雙令牌
const newAccessToken = generateToken(userData, '1h');
const newRefreshToken = generateToken(userData, '7d');
res.json({ access_token: newAccessToken, refresh_token: newRefreshToken });
} catch (error) {
res.status(401).send('令牌已失效');
}
})
五、完整代碼
1. 前端代碼
<template>
<div v-if="!isLogin">
<button @click="login">登錄</button>
</div>
<div v-else>
<h1>登錄成功</h1>
<p>歡迎回來(lái),{{ username }}</p>
<p>您的郵箱:{{ email }}</p>
</div>
<!-- home -->
<div v-if="isLogin">
<button @click="getHomeData">獲取首頁(yè)數(shù)據(jù)</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { userLogin, getHomeDataApi } from './api.js'
const isLogin = ref(false)
const username = ref('')
const email = ref('')
const password = ref('')
const login = async() => {
username.value = 'zs'
email.value = '123@qq.com'
password.value = '123'
const res = await userLogin({username: username.value, email: email.value, password: password.value})
console.log(res)
const {access_token, refresh_token, userInfo} = res.data
if (access_token) {
isLogin.value = true
}
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
}
const getHomeData = async() => {
const res = await getHomeDataApi()
console.log(res)
}
</script>
<style lang="css" scoped>
</style>
// api.js
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:3000',
timeout: 3000,
})
// 請(qǐng)求攔截器
api.interceptors.request.use(config => {
const access_token = localStorage.getItem('access_token');
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
})
// 響應(yīng)攔截器
api.interceptors.response.use(
(response) => {
return response
},
async (error) => { // 響應(yīng)失敗
const { data, status, config } = error.response;
if (status === 401 && config.url !== '/refresh') {
// 刷新token
const res = await refreshToken()
if (res.status === 200) { // token刷新成功
// 重新將剛剛失敗的請(qǐng)求發(fā)送出去
return api(config)
} else {
// 重定向到登錄頁(yè) router.push('/login')
window.location.href = '/login'
}
}
}
)
export const userLogin = (data) => {
return api.post('/login', data)
}
export const getHomeDataApi = () => {
return api.get('/home')
}
async function refreshToken() {
const res = await api.get('/refresh', {
params: {
token: localStorage.getItem('refresh_token')
}
})
localStorage.setItem('access_token', res.data.access_token)
localStorage.setItem('refresh_token', res.data.refresh_token)
return res
}
2. 后端代碼
server.js
const express = require('express');
const app = express();
const port = 3000;
app.use(express.json()); // 解析 JSON 格式的請(qǐng)求體
const jwtToken = require('./token.js');
const cors = require('cors');
app.use(cors())
const users = [
{ username: 'zs', password: '123', email: '123@qq.com' },
{ username: 'ls', password: '456', email: '456@qq.com' }
]
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (!user) {
return res.status(404).json({status: 'error', message: '用戶不存在'});
}
if (user.password !== password) {
return res.status(401).json({status: 'error', message: '密碼錯(cuò)誤'});
}
// 生成兩個(gè) token
const access_token = jwtToken.generateToken(user, '1h');
const refresh_token = jwtToken.generateToken(user, '7d');
res.json({
userInfo: {
username: user.username,
email: user.email
},
access_token,
refresh_token
})
})
// 需要token 認(rèn)證的路由
app.get('/home', (req, res) => {
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(401).json({status: 'error', message: '未登錄'});
}
try {
const token = authorization.split(' ')[1]; // 'Bearer esdadfadadxxxxxxxxx'
const data = jwtToken.verifyToken(token);
res.json({ status: 'success', message: '驗(yàn)證成功', data: data });
} catch (error) {
return res.status(401).json({status: error, message: 'token失效,請(qǐng)重新登錄'});
}
})
// 刷新 token
app.get('/refresh', (req, res) => {
const { token } = req.query;
try {
const data = jwtToken.verifyToken(token);
const access_token = jwtToken.generateToken(data, '1h');
const refresh_token = jwtToken.generateToken(data, '7d');
res.json({ status: 'success', message: '刷新成功', access_token, refresh_token });
} catch (error) {
return res.status(401).json({status: error, message: 'token失效,請(qǐng)重新登錄'});
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
})
// token.js
const jwt = require('jsonwebtoken');
// 生成 token
function generateToken(user, expiresIn) {
const payload = {
username: user.username,
email: user.email
};
const secret = 'my_secret_key';
const options = {
expiresIn: expiresIn
};
return jwt.sign(payload, secret, options);
}
// 驗(yàn)證 token
function verifyToken(token) {
const secret = 'my_secret_key';
const decoded = jwt.verify(token, secret);
return decoded;
}
module.exports = {
generateToken,
verifyToken
};
六、流程圖解
用戶發(fā)起請(qǐng)求 → 攜帶access_token → 服務(wù)端驗(yàn)證
↓ 無(wú)效/過(guò)期
觸發(fā)401錯(cuò)誤 → 前端攔截 → 發(fā)起refresh_token刷新請(qǐng)求
↓ 刷新成功
更新本地令牌 → 重新發(fā)送原請(qǐng)求 → 用戶無(wú)感知
↓ 刷新失敗
跳轉(zhuǎn)登錄頁(yè)面 → 需要重新認(rèn)證
七、安全注意事項(xiàng)
- refresh_token要長(zhǎng)期有效,但也不能太長(zhǎng):通常設(shè)置7-30天有效期
- 使用HTTPS:防止令牌被中間人竊取
- 不要明文存儲(chǔ)令牌:使用瀏覽器localStorage要確保XSS防護(hù)
- 設(shè)置合理有效期:根據(jù)業(yè)務(wù)需求平衡安全與體驗(yàn)
到此這篇關(guān)于雙Token實(shí)現(xiàn)無(wú)感刷新的文章就介紹到這了,更多相關(guān)雙Token實(shí)現(xiàn)無(wú)感刷新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 前端登錄token失效實(shí)現(xiàn)雙Token無(wú)感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無(wú)感刷新
- 雙token無(wú)感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
- node.js實(shí)現(xiàn)雙Token+Cookie存儲(chǔ)+無(wú)感刷新機(jī)制的示例
- 雙Token無(wú)感刷新機(jī)制實(shí)現(xiàn)方式
- 前端雙token無(wú)感刷新圖文詳解
- vue中雙token和無(wú)感刷新token的區(qū)別
- Vue實(shí)現(xiàn)雙token無(wú)感刷新的示例代碼
- Vue3+Vite使用雙token實(shí)現(xiàn)無(wú)感刷新
- SpringBoot+React中雙token實(shí)現(xiàn)無(wú)感刷新
相關(guān)文章
js使用visibilitychange處理頁(yè)面關(guān)閉事件
本文主要介紹了js使用visibilitychange處理頁(yè)面關(guān)閉事件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Javascript連接多個(gè)數(shù)組不用concat來(lái)解決
這篇文章主要介紹了不用concat解決Javascript連接多個(gè)數(shù)組,需要的朋友可以參考下2014-03-03
原生js實(shí)現(xiàn)autocomplete插件
這篇文章主要介紹了原生js實(shí)現(xiàn)autocomplete插件的相關(guān)資料,需要的朋友可以參考下2016-04-04
使用bat打開多個(gè)cmd窗口執(zhí)行g(shù)ulp、node
本文主要介紹了使用bat打開多個(gè)cmd窗口執(zhí)行g(shù)ulp、node的方法。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02
js實(shí)現(xiàn)把時(shí)間戳轉(zhuǎn)換為yyyy-MM-dd hh:mm 格式(es6語(yǔ)法)
下面小編就為大家分享一篇js實(shí)現(xiàn)把時(shí)間戳轉(zhuǎn)換為yyyy-MM-dd hh:mm 格式(es6語(yǔ)法),具有很的參考價(jià)值,希望對(duì)大家有所幫助2017-12-12
(function(){})()的用法與優(yōu)點(diǎn)
(function(){})()的用法與優(yōu)點(diǎn)...2007-03-03
JS學(xué)習(xí)筆記之貪吃蛇小游戲demo實(shí)例詳解
這篇文章主要介紹了JS學(xué)習(xí)筆記之貪吃蛇小游戲demo,結(jié)合實(shí)例形式詳細(xì)分析了javascript實(shí)現(xiàn)貪吃蛇小游戲的原理、步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-05-05
JS獲取字符對(duì)應(yīng)的ASCII碼實(shí)例
下面小編就為大家?guī)?lái)一篇JS獲取字符對(duì)應(yīng)的ASCII碼實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就想給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09

