Vue3+Vite使用雙token實現無感刷新

前言

近期寫的一個項目使用雙token實現無感刷新。最後做瞭一些總結,本文詳細介紹瞭實現流程,前後端詳細代碼。前端使用瞭Vue3+Vite,主要是axios封裝,服務端使用瞭koa2做瞭一個簡單的服務器模擬。

一、token 登錄鑒權

jwt:JSON Web Token。是一種認證協議,一般用來校驗請求的身份信息和身份權限。 由三部分組成:Header、Hayload、Signature

header:也就是頭部信息,是描述這個 token 的基本信息,json 格式

{
  "alg": "HS256", // 表示簽名的算法,默認是 HMAC SHA256(寫成 HS256)
  "type": "JWT" // 表示Token的類型,JWT 令牌統一寫為JWT
}

payload:載荷,也是一個 JSON 對象,用來存放實際需要傳遞的數據。不建議存放敏感信息,比如密碼。

{
  "iss": "a.com", // 簽發人
  "exp": "1d", // expiration time 過期時間
  "sub": "test", // 主題
  "aud": "", // 受眾
  "nbf": "", // Not Before 生效時間
  "iat": "", // Issued At 簽發時間
  "jti": "", // JWT ID 編號
  // 可以定義私有字段
  "name": "",
  "admin": ""
}

Signature 簽名 是對前兩部分的簽名,防止數據被篡改。 需要指定一個密鑰。這個密鑰隻有服務器才知道,不能泄露。使用 Header 裡面指定的簽名算法,按照公式產生簽名。

算出簽名後,把 Header、Payload、Signature 三個部分拼成的一個字符串,每個部分之間用 . 分隔。這樣就生成瞭一個 token

二、何為雙 token

  • accessToken:用戶獲取數據權限
  • refreshToken:用來獲取新的accessToken

雙 token 驗證機制,其中 accessToken 過期時間較短,refreshToken 過期時間較長。當 accessToken 過期後,使用 refreshToken 去請求新的 token。

雙 token 驗證流程

  • 用戶登錄向服務端發送賬號密碼,登錄失敗返回客戶端重新登錄。登錄成功服務端生成 accessToken 和 refreshToken,返回生成的 token 給客戶端。
  • 在請求攔截器中,請求頭中攜帶 accessToken 請求數據,服務端驗證 accessToken 是否過期。token 有效繼續請求數據,token 失效返回失效信息到客戶端。
  • 客戶端收到服務端發送的請求信息,在二次封裝的 axios 的響應攔截器中判斷是否有 accessToken 失效的信息,沒有返回響應的數據。有失效的信息,就攜帶 refreshToken 請求新的 accessToken。
  • 服務端驗證 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客戶端,無效,返回無效信息給客戶端。
  • 客戶端響應攔截器判斷響應信息是否有 refreshToken 有效無效。無效,退出當前登錄。有效,重新存儲新的 token,繼續請求上一次請求的數據。

註意事項

  • 短token失效,服務端拒絕請求,返回token失效信息,前端請求到新的短token如何再次請求數據,達到無感刷新的效果。
  • 服務端白名單,成功登錄前是還沒有請求到token的,那麼如果服務端攔截請求,就無法登錄。定制白名單,讓登錄無需進行token驗證。

三、服務端代碼

1. 搭建koa2服務器

全局安裝koa腳手架

npm install koa-generator -g

創建服務端 直接koa2+項目名

koa2 server

cd server 進入到項目安裝jwt

npm i jsonwebtoken

為瞭方便直接在服務端使用koa-cors 跨域

npm i koa-cors

在app.js中引入應用cors

const cors=require('koa-cors')
...
app.use(cors())

2. 雙token

新建utils/token.js

const jwt=require('jsonwebtoken')

const secret='2023F_Ycb/wp_sd'  // 密鑰
/*
expiresIn:5 過期時間,時間單位是秒
也可以這麼寫 expiresIn:1d 代表一天 
1h 代表一小時
*/
// 本次是為瞭測試,所以設置時間 短token5秒 長token15秒
const accessTokenTime=5  
const refreshTokenTime=15 

// 生成accessToken
const setAccessToken=(payload={})=>{  // payload 攜帶用戶信息
    return jwt.sign(payload,secret,{expireIn:accessTokenTime})
}
//生成refreshToken
const setRefreshToken=(payload={})=>{
    return jwt.sign(payload,secret,{expireIn:refreshTokenTime})
}

module.exports={
    secret,
    setAccessToken,
    setRefreshToken
}

3. 路由

直接使用腳手架創建的項目已經在app.js使用瞭路由中間件 在router/index.js 創建接口

const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const { getAccesstoken, getRefreshtoken, secret }=require('../utils/token')

/*登錄接口*/
router.get('/login',()=>{
    let code,msg,data=null
    code=2000
    msg='登錄成功,獲取到token'
    data={
        accessToken:getAccessToken(),
        refreshToken:getReferToken()
    }
    ctx.body={
        code,
        msg,
        data
    }
})

/*用於測試的獲取數據接口*/
router.get('/getTestData',(ctx)=>{
    let code,msg,data=null
    code=2000
    msg='獲取數據成功'
    ctx.body={
        code,
        msg,
        data
    }
})

/*驗證長token是否有效,刷新短token
  這裡要註意,在刷新短token的時候回也返回新的長token,延續長token,
  這樣活躍用戶在持續操作過程中不會被迫退出登錄。長時間無操作的非活
  躍用戶長token過期重新登錄
*/
router.get('/refresh',(ctx)=>{
    let code,msg,data=null
    //獲取請求頭中攜帶的長token
    let r_tk=ctx.request.headers['pass']
    //解析token 參數 token 密鑰 回調函數返回信息
    jwt.verify(r_tk,secret,(error)=>{
        if(error){
            code=4006,
            msg='長token無效,請重新登錄'
        } else{
            code=2000,
            msg='長token有效,返回新的token',
            data={
                accessToken:getAccessToken(),
                refreshToken:getReferToken()
            }
        }
    })
})

4. 應用中間件

utils/auth.js

const { secret } = require('./token')
const jwt = require('jsonwebtoken')

/*白名單,登錄、刷新短token不受限制,也就不用token驗證*/
const whiteList=['/login','/refresh']
const isWhiteList=(url,whiteList)=>{
        return whiteList.find(item => item === url) ? true : false
}

/*中間件
 驗證短token是否有效
*/
const cuth = async (ctx,next)=>{
    let code, msg, data = null
    let url = ctx.path
    if(isWhiteList(url,whiteList)){
        // 執行下一步
        return await next()
    } else {
        // 獲取請求頭攜帶的短token
        const a_tk=ctx.request.headers['authorization']
        if(!a_tk){
            code=4003
            msg='accessToken無效,無權限'
            ctx.body={
                code,
                msg,
                data
            }
        } else{
            // 解析token
            await jwt.verify(a_tk,secret.(error)=>{
                if(error)=>{
                      code=4003
                      msg='accessToken無效,無權限'
                      ctx.body={
                          code,
                          msg,
                          datta
                      }
                } else {
                    // token有效
                    return await next()
                }
            })
        }
    }
}
module.exports=auth

在app.js中引入應用中間件

const auth=requier(./utils/auth)
···
app.use(auth)

其實如果隻是做一個簡單的雙token驗證,很多中間件是沒必要的,比如解析靜態資源。不過為瞭節省時間,方便就直接使用瞭koa2腳手架。

最終目錄結構:

四、前端代碼

1. Vue3+Vite框架

前端使用瞭Vue3+Vite的框架,看個人使用習慣。

npm init vite@latest client_side

安裝axios

npm i axios

2. 定義使用到的常量

config/constants.js

export const ACCESS_TOKEN = 'a_tk' // 短token字段
export const REFRESH_TOKEN = 'r_tk' // 短token字段
export const AUTH = 'Authorization'  // header頭部 攜帶短token
export const PASS = 'pass' // header頭部 攜帶長token

3. 存儲、調用過期請求

關鍵點:把攜帶過期token的請求,利用Promise存在數組中,保持pending狀態,也就是不調用resolve()。當獲取到新的token,再重新請求。 utils/refresh.js

export {REFRESH_TOKEN,PASS} from '../config/constants.js'
import { getRefreshToken, removeRefreshToken, setAccessToken, setRefreshToken} from '../config/storage'

let subsequent=[]
let flag=false // 設置開關,保證一次隻能請求一次短token,防止客戶多此操作,多次請求

/*把過期請求添加在數組中*/
export const addRequest = (request) => {
    subscribes.push(request)
}

/*調用過期請求*/
export const retryRequest = () => {
    console.log('重新請求上次中斷的數據');
    subscribes.forEach(request => request())
    subscribes = []
}

/*短token過期,攜帶token去重新請求token*/
export const refreshToken=()=>{
    if(!flag){
        flag = true;
        let r_tk = getRefershToken() // 獲取長token
        if(r_tk){
            server.get('/refresh',Object.assign({},{
                headers:{[PASS]=r_tk}
            })).then((res)=>{
                //長token失效,退出登錄
                if(res.code===4006){
                    flag = false
                    removeRefershToken(REFRESH_TOKEN)
                } else if(res.code===2000){
                    // 存儲新的token
                    setAccessToken(res.data.accessToken)
                    setRefreshToken(res.data.refreshToken)
                    flag = false
                    // 重新請求數據
                    retryRequest()
                }
            })
        }
    }
}

4. 封裝axios

utlis/server.js

import axios from "axios";
import * as storage from "../config/storage"
import * as constants from '../config/constants'
import { addRequest, refreshToken } from "./refresh";

const server = axios.create({
    baseURL: 'http://localhost:3004', // 你的服務器
    timeout: 1000 * 10,
    headers: {
        "Content-type": "application/json"
    }
})

/*請求攔截器*/
server.interceptors.request.use(config => {
    // 獲取短token,攜帶到請求頭,服務端校驗
    let aToken = storage.getAccessToken(constants.ACCESS_TOKEN)
    config.headers[constants.AUTH] = aToken
    return config
})

/*響應攔截器*/
server.interceptors.response.use(
    async response => {
        // 獲取到配置和後端響應的數據
        let { config, data } = response
        console.log('響應提示信息:', data.msg);
        return new Promise((resolve, reject) => {
            // 短token失效
            if (data.code === 4003) {
                // 移除失效的短token
                storage.removeAccessToken(constants.ACCESS_TOKEN)
                // 把過期請求存儲起來,用於請求到新的短token,再次請求,達到無感刷新
                addRequest(() => resolve(server(config)))
                // 攜帶長token去請求新的token
                refreshToken()
            } else {
                // 有效返回相應的數據
                resolve(data)
            }

        })

    },
    error => {
        return Promise.reject(error)
    }
)

5. 復用封裝

import * as constants from "./constants"

// 存儲短token
export const setAccessToken = (token) => localStorage.setItem(constanst.ACCESS_TOKEN, token)
// 存儲長token
export const setRefershToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token)
// 獲取短token
export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN)
// 獲取長token
export const getRefershToken = () => localStorage.getItem(constants.REFRESH_TOKEN)
// 刪除短token
export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN)
// 刪除長token
export const removeRefershToken = () => localStorage.removeItem(constants.REFRESH_TOKEN)

6. 接口封裝

apis/index.js

import server from "../utils/server";
/*登錄*/
export const login = () => {
    return server({
        url: '/login',
        method: 'get'
    })
}
/*請求數據*/
export const getData = () => {
    return server({
        url: '/getList',
        method: 'get'
    })
}

項目運行

最後的最後,運行項目,查看效果 後端設置的短token5秒,長token10秒。登錄請求到token後,請求數據可以正常請求,五秒後再次請求,短token失效,這時長token有效,請求到新的token,refresh接口隻調用瞭一次。長token也過期後,就需要重新登錄啦。

寫在最後

這就是一整套的前後端使用雙token機制實現無感刷新。token能做到的還有很多,比如權限管理、同一賬號異地登錄。本文隻是淺顯的應用瞭一下。

到此這篇關於Vue3+Vite使用雙token實現無感刷新的文章就介紹到這瞭,更多相關Vue3 無感刷新內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: