雙Token實現(xiàn)無感刷新的完整代碼示例
一、為什么需要無感刷新?
想象一下你正在刷視頻,突然提示"登錄已過期,請重新登錄",需要退出當前頁面重新輸入密碼。這樣的體驗非常糟糕!無感刷新就是為了解決這個問題:讓用戶在不知不覺中完成身份續(xù)期,保持長時間在線狀態(tài)。
二、雙Token機制原理
我們使用兩個令牌:
- 短令牌:access_token(1小時):用于日常請求
- 長令牌:refresh_token(7天):專門用來刷新令牌
工作流程:
用戶登錄 → 獲取雙令牌 → access_token過期 → 用refresh_token獲取新的雙令牌 → 自動續(xù)期
三、前端實現(xiàn)(Vue + Axios)
1. 登錄存儲令牌
const login = async () => { const res = await userLogin(user); //賬號密碼 // 保存雙令牌到本地 localStorage.setItem('access_token', res.access_token); localStorage.setItem('refresh_token', res.refresh_token); }
2. 請求自動攜帶令牌
通過請求攔截器自動添加認證頭:
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登錄過期的錯誤時自動請求刷新
驗證長令牌是否失效
- 失效重定向到登錄頁面
- 未失效重新獲取雙令牌并重新發(fā)起請求
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() // 校驗的函數(shù) if (res.status === 200) { // token刷新成功 // 重新將剛剛失敗的請求發(fā)送出去 return api(config) } else { // 重定向到登錄頁 router.push('/login') window.location.href = '/login' } } } )
四、后端實現(xiàn)(Node.js + Express)
1. 生成雙令牌
// 生成1小時有效的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 { // 驗證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>歡迎回來,{{ username }}</p> <p>您的郵箱:{{ email }}</p> </div> <!-- home --> <div v-if="isLogin"> <button @click="getHomeData">獲取首頁數(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, }) // 請求攔截器 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刷新成功 // 重新將剛剛失敗的請求發(fā)送出去 return api(config) } else { // 重定向到登錄頁 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 格式的請求體 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: '密碼錯誤'}); } // 生成兩個 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 認證的路由 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: '驗證成功', data: data }); } catch (error) { return res.status(401).json({status: error, message: 'token失效,請重新登錄'}); } }) // 刷新 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失效,請重新登錄'}); } }) 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); } // 驗證 token function verifyToken(token) { const secret = 'my_secret_key'; const decoded = jwt.verify(token, secret); return decoded; } module.exports = { generateToken, verifyToken };
六、流程圖解
用戶發(fā)起請求 → 攜帶access_token → 服務(wù)端驗證
↓ 無效/過期
觸發(fā)401錯誤 → 前端攔截 → 發(fā)起refresh_token刷新請求
↓ 刷新成功
更新本地令牌 → 重新發(fā)送原請求 → 用戶無感知
↓ 刷新失敗
跳轉(zhuǎn)登錄頁面 → 需要重新認證
七、安全注意事項
- refresh_token要長期有效,但也不能太長:通常設(shè)置7-30天有效期
- 使用HTTPS:防止令牌被中間人竊取
- 不要明文存儲令牌:使用瀏覽器localStorage要確保XSS防護
- 設(shè)置合理有效期:根據(jù)業(yè)務(wù)需求平衡安全與體驗
到此這篇關(guān)于雙Token實現(xiàn)無感刷新的文章就介紹到這了,更多相關(guān)雙Token實現(xiàn)無感刷新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
取消Bootstrap的dropdown-menu點擊默認關(guān)閉事件方法
今天小編就為大家分享一篇取消Bootstrap的dropdown-menu點擊默認關(guān)閉事件方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08javascript將數(shù)字轉(zhuǎn)換整數(shù)金額大寫的方法
這篇文章主要介紹了javascript將數(shù)字轉(zhuǎn)換整數(shù)金額大寫的方法,通過自定義函數(shù)中的數(shù)組替換實現(xiàn)數(shù)字轉(zhuǎn)換整數(shù)金額大寫的功能,非常具有實用價值,需要的朋友可以參考下2015-01-01