无感刷新页面(附可运行的前后端源码,前端vue,后端node)

2025-12-13 0 202

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 效果图无感刷新页面(附可运行的前后端源码,前端vue,后端node)

无感刷新页面(附可运行的前后端源码,前端vue,后端node)

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

长风破浪会有时,直挂云帆济沧海

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 无感刷新页面(附可运行的前后端源码,前端vue,后端node) https://www.zuozi.net/36439.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务