首页 > 编程语言 >每日一练:无感刷新页面(附可运行的前后端源码,前端vue,后端node)

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

时间:2023-10-27 11:32:51浏览次数:44  
标签:node code const ctx token 源码 msg data 无感

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 one rror = 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)_服务端

3、完整项目代码

3.1 地址

https://github.com/heyu3913/doubleToken

3.2 运行

后端:

cd server
pnpm i
pnpm start

前端

cd my-vue-app
pnpm i
pnpm dev

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



标签:node,code,const,ctx,token,源码,msg,data,无感
From: https://blog.51cto.com/u_15961676/8052055

相关文章

  • 每日一题:吃透大文件上传问题(附可运行的前后端源码)
    大文件上传前言在日常开发中,文件上传是常见的操作之一。文件上传技术使得用户可以方便地将本地文件上传到Web服务器上,这在许多场景下都是必需的,比如网盘上传、头像上传等。但是当我们需要上传比较大的文件的时候,容易碰到以下问题:上传时间比较久中间一旦出错就需要重新上传一般服务......
  • 常见编译优化及LLVM Pass源码分析(填坑ing)
    一、常量传播二、常量折叠三、复写传播四、公共子表达式消除五、死代码消除(ADCE,Aggressivedeadcodeelimination)llvm/lib/Transforms/Scalar/ADCE.cpp六、函数内联......
  • Redis 7.0 源码环境搭建与阅读技巧
    天下武功,无坚不摧,唯快不破!我的名字叫Redis,全称是RemoteDictionaryServer。有人说,组CP,除了要了解她外,还要给机会让她了解你。那么,作为开发工程师的你,是否愿意认真阅读此心法抓住机会来了解我,运用到你的系统中提升性能。我遵守BSD协议,由意大利人SalvatoreSanfilippo......
  • 基于SpringBoot的菜谱交流系统-计算机毕业设计源码+LW文档
    开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9浏览器:谷歌浏览器本选题致力于开发一个菜谱交流系统,旨在帮助越来越多的人可以与他人分享自己做菜的经验和做法,并......
  • 基于Scrapy的考研院校报名数据分析系统-计算机毕业设计源码+LW文档
    一、选题意义随着我国高等教育不断发展,现在我国学生本科就读率不断增高,社会上本科生人才数量也在不断增多。由于我国人口众多,从恢复高考到现在,全国本科学历占比不超过5%,但总数也达到了六千万人左右。本科生不断增多,也造成了就业竞争加大,于是选择继续深造考研的大学生不断增多,2022......
  • 基于Spring Boot的房产中介系统-计算机毕业设计源码+LW文档
    1.依据及意义:随着国家经济地快速增长以及城镇化步伐地加快,资讯的获取途径主要来源于互联网,而当前中国房地产市场新盘房源,现实生活中人们对对房屋的需求急剧增加。然而现代社会仍然会存在较多的中介部门,这些中介部门通常对外发布许多住房方面的资讯。可是,由于住房中介信息相对较多......
  • node开发一个接口详细步骤
    最近在做后台系统改版,由于目前接口还没出来,就自己用nodejs写了个简单的接口。我这里用的是nodejs+mysql的这里不讲nodejs和mysql的安装。这些基础略过。首先创建文件夹。cd进入文件。npminit进行初始化安装下面的包npminstallbody-parserexpressmysqlcors-S这里下......
  • node+mysql+express实现登录/注册/修改密码/删除用户 接口
    实现用户的注册、登录、修改密码、删除用户操作用到的数据库:nodecms;表:user目录结构:db目录下存放数据库操作语句:userSQL.js用户有关的操作语句router目录接口路由文件user.js用户接口路由connect.js数据库连接index.html前端测试页面index.js入口文件package.js......
  • 【源码】面向区域医疗和集团医院设计开发的基于云计算的区域LIS系统
    一套具有区域内标本流转、检验报告共享、结果互认的区域LIS系统,可以节约小型医院购买检验设备的成本,减少重复检查。充分利用云技术优势、面向区域医疗和集团医院设计开发的基于云计算的区域LIS系统。 云LIS源码技术说明:技术架构:ASP.NETCORE3.1MVC+SQLserver+Redis等 开发......
  • 在线直播源码,Dialog使用详情(中间弹框)
    在线直播源码,Dialog使用详情(中间弹框)第一步:应用程序入口设置当我们导入依赖后,在应用程序顶层把GetMaterialApp作为顶层,如下所示 import'package:flutter/material.dart';import'package:flutter_getx_example/DialogExample/DialogExample.dart';import'package:get/ge......