每日一練:無感重新整理頁面(附可執行的前後端原始碼,前端vue,後端node)

2023-09-15 18:00:31

1、前言

想象下,你正常在網頁上瀏覽頁面。突然彈出一個視窗,告訴你登入失效,跳回了登入頁面,讓你重新登入。你是不是很惱火。這時候無感重新整理的作用就體現出來了。

2、方案

2.1 redis設定過期時間

在最新的技術當中,token一般都是在Redis伺服器存著,設定過期時間。只要在有效時間內,重新發出請求,Redis中的過期時間會去更新,這樣前只需要一個token。這個方案一般是後端做。

2.2 雙token模式

2.21 原理

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

2.22 上程式碼

後端:node.js、koa2伺服器、jwt、koa-cors等(可使用koa腳手架建立專案,本專案基於koa腳手架建立。完整程式碼可見文章末尾github地址)

  1. 新建utils/token.js (雙token)
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 accessToken=(payload={})=>{  // payload 攜帶使用者資訊
    return jwt.sign(payload,secret,{expiresIn:accessTokenTime})
}
//生成refreshToken
const refreshToken=(payload={})=>{
    return jwt.sign(payload,secret,{expiresIn:refreshTokenTime})
}

module.exports={
    secret,
    accessToken,
    refreshToken
}
  1. router/index.js 建立路由介面
const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const { accessToken, refreshToken, secret }=require('../utils/token')
router.get('/', async (ctx, next) => {
  await ctx.render('index', {
    title: 'Hello Koa 2!'
  })
})

router.get('/string', async (ctx, next) => {
  ctx.body = 'koa2 string'
})

router.get('/json', async (ctx, next) => {
  ctx.body = {
    title: 'koa2 json'
  }
})
/*登入介面*/
router.get('/login',(ctx)=>{
  let code,msg,data=null
  code=2000
  msg='登入成功,獲取到token'
  data={
    accessToken:accessToken(),
    refreshToken:refreshToken()
  }
  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: accessToken(),
        refreshToken: refreshToken()
      }
    }
    ctx.body={
      code,
      msg:msg?msg:null,
      data
    }
  })
})



module.exports = router

3.新建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 auth = 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,async (error)=>{
                if(error){
                    code=4003
                    msg='accessToken無效,無許可權'
                    ctx.body={
                        code,
                        msg,
                        data
                    }
                } else {
                    // token有效
                    return await next()
                }
            })
        }
    }
}
module.exports=auth
  1. app.js
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const cors=require('koa-cors')

const index = require('./routes/index')
const users = require('./routes/users')
const auth=require('./utils/auth')

// error handler
onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(cors())
app.use(auth)

app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

前端:vite、vue3、axios等 (完整程式碼可見文章末尾github地址)

  1. 新建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
  1. 新建config/storage.js
import * as constants from "./constants"

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

3.新建utils/refresh.js

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

let subscribes=[]
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=()=>{
    console.log('flag--',flag)
    if(!flag){
        flag = true;
        let r_tk = getRefreshToken() // 獲取長token
        if(r_tk){
            server.get('/refresh',Object.assign({},{
                headers:{PASS : r_tk}
            })).then((res)=>{
                //長token失效,退出登入
                if(res.code===4006){
                    flag = false
                    removeRefreshToken(REFRESH_TOKEN)
                } else if(res.code===2000){
                    // 儲存新的token
                    setAccessToken(res.data.accessToken)
                    setRefreshToken(res.data.refreshToken)
                    flag = false
                    // 重新請求資料
                    retryRequest()
                }
            })
        }
    }
}

4.新建utils/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:3000', // 你的伺服器
    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)
    }
)
export default  server

5.新建apis/index.js

import server from "../utils/server.js";
/*登入*/
export const login = () => {
    return server({
        url: '/login',
        method: 'get'
    })
}
/*請求資料*/
export const getList = () => {
    return server({
        url: '/getTestData',
        method: 'get'
    })
}

6.修改App.vue

<script setup>
  import {login,getList} from "./apis";
  import {setAccessToken,setRefreshToken} from "./config/storage";
  const getToken=()=>{
    login().then(res=>{
      setAccessToken(res.data.accessToken)
      setRefreshToken(res.data.refreshToken)
    })
  }
  const getData = ()=>{
    getList()
  }
</script>

<template>
    <button @click="getToken">登入</button>
    <button @click="getData">請求資料</button>

</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

2.23 效果圖

3、完整專案程式碼

3.1 地址

https://github.com/heyu3913/doubleToken

3.2 執行

後端:

cd server
pnpm i
pnpm start

前端

cd my-vue-app
pnpm i
pnpm dev

4 PS: 這裡附送大家一個免費的gpt地址,自己搭的,不收費。註冊即用:

https://www.hangyejingling.cn