vue 前端关于token的刷新(进阶)

当用户登录后的token过期,但仍在操作时,需要自动刷新token以避免页面请求失败。通过监听用户操作并判断是否需要刷新token,在发送请求时根据refresh_token状态决定是否先更新token再继续执行原请求。通常建议登录时返回两个token,一个用于业务,一个用于刷新,以增强安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

token刷新

用户在登录两个小时后,或许还会继续操作,但是一般token是有时效的,这时候如果遇到刷新页面就会请求失败,但显然这种效果不是我们想要,那么我们就要刷新token,让用户在刷新页面时带的是新token就可以继续操作页面

例子需求

1. 页面超过半个小时不操作,自动退出登录。
2. 若操作到token时效的两小时后,还有继续操作的意愿,将不会退出。

逻辑原理

login.vue文件:参数定义
//生成token的时间
accessToken_time
//最后一次操作页面的时间
lastOperationTime
//是否需要刷新token的标示  1时要刷新 0时不需要
refresh_token

// 登录后设置各参数以及数值
this.$https.post('api/user/login', postData).then((res) => {
   let now = new Date().getTime()
   localStorage.setItem('lastOperationTime', now)
   localStorage.setItem('refresh_token', 0)
   localStorage.setItem('accessToken_time', now) // 生成token时间
})

lastOperationTime用于判断用户是否已经半个小时操作,是,直接退出,否,则判断是否到了需要更换token的时间,更新参数refresh_token的状态

nowTime-lastOperationTime判断是否已经半个小时未操作
清理登录信息后退出登录
更新refresh_token状态

APP.vue添加监听

<template>
  <div id="app">
    <router-view />
  </div>
</template>
<script>
export default {
  name: 'App',
  },
  created () {
    let that = this;
    // 全局监听事件
    window.addEventListener("click", (e) => {
      // console.log('页面点击')
      if (this.$route.path != '/') {
        let lastOperationTime = localStorage.getItem('lastOperationTime')
        let now = new Date().getTime();
        // debugger;
        if (lastOperationTime) {
          let timeDifference = (now - Number(lastOperationTime));
          if (timeDifference > 1000 * 60 * 30) { // 超过30分未动直接退出
            localStorage.setItem('refresh_token', 0)
            that.$message('操作超时请重新登录')
            setTimeout(() => {
              // that.$router.push('/')
              that.loginOut()
            }, 2000)
          } else {
            localStorage.setItem('lastOperationTime', now);
            let accessToken_time = localStorage.getItem('accessToken_time')
            let timeDifference2 = (now - Number(accessToken_time));
            if (timeDifference2 > 1.8 * 60 * 60 * 1000) { // 大于两分钟 
              localStorage.setItem('refresh_token', 1)
            } else {
              localStorage.setItem('refresh_token', 0)
            }
          }
        }
      }
    }, true)
  },
  methods: {
    loginOut () {
      localStorage.clear()
      if (window.api.outUrl != '') {
        window.location.href = window.api.outUrl
      } else {
        this.$router.push('/')
      }
    },
  }
}
</script>

刷新token后继续调用请求原理

在发送请求时根据参数 refresh_token == 1 时进行token刷新,将要请求的http请求用promise挂起来,发送一个刷新token的请求,在token更新后,更新标志参数refresh_token == 0以及token生成的时间accessToken_time,然后继续执行之前挂起的http请求

http.js 文件
/* eslint-disable */
import axios from "axios";
import qs from "qs";
import { setTimeout } from 'timers'
import { Message } from 'element-ui';

const httpService = axios.create({
  baseURL: window.api.url,
  timeout: 600000,
  responseType: `json`,
  transformRequest: [data => qs.stringify(data)],
  headers: {
    "Conten-Type": `appplication/x-www-form-urlencoded;charset=utf-8`
  }

});

/*是否有请求正在刷新token*/
window.isRefreshing = false
// 挂起的请求数组
let refreshSubscribers = []

/*push所有请求到数组中*/
function subscribeTokenRefresh (cb) {
  refreshSubscribers.push(cb)
}

/*刷新请求(refreshSubscribers数组中的请求得到新的token之后会自执行,用新的token去请求数据)*/
function onRrefreshed (token) {
  refreshSubscribers.map(cb => cb(token))
}

httpService.interceptors.request.use(
  config => {
    return config;
  },
  error => Promise.reject(error)
);
httpService.interceptors.request.use(
  config => {
    let accessToken = localStorage.getItem('dvm_accessToken');
    if (accessToken && accessToken !== '') {
      config.headers.common['Authorization'] = accessToken;
    }
    // 判断是否需要更新token
    let refresh_token = localStorage.getItem('refresh_token')
    if (refresh_token && refresh_token == 1 && config.url.indexOf('api/token/refreshToken') === -1) {
      if (!window.isRefreshing) {
        /*将刷新token的标志置为true*/
        window.isRefreshing = true
        config.headers.common['Authorization'] = accessToken;
        /*发起刷新token的请求*/
        get('api/token/refreshToken').then(res => {
          localStorage.setItem('accessToken_time', new Date().getTime())
          localStorage.setItem('refresh_token', 0)
           /*将标志置为false*/
          window.isRefreshing = false
          /*成功刷新token*/
          config.headers.common['Authorization'] = res.result;
          /*更新auth*/
          // localStorage.setItem('auth', JSON.stringify(res.data.data))
          localStorage.setItem('dvm_accessToken', res.result);
          /*执行数组里的函数,重新发起被挂起的请求*/
          onRrefreshed(res.result)
          /*执行onRefreshed函数后清空数组中保存的请求*/
          refreshSubscribers = []
        }).catch(err => {
          alert(err.response.data.message)
          /*清除本地保存的auth*/
          // localStorage.removeItem('auth')
          window.location.href = '/'
        })
      }
      let retry = new Promise((resolve, reject) => {
        /*(token) => {...}这个函数就是回调函数*/
        subscribeTokenRefresh((token) => {
          config.headers.common['Authorization'] = token
          /*将请求挂起*/
          resolve(config)
        })
      })
      return retry
    }
    return config;
  },
  error => Promise.reject(error)
);

httpService.interceptors.response.use(
  response => {
    let accessToken = response.headers['authorization'];
    if (accessToken && accessToken !== '') {
      localStorage.setItem('dvm_accessToken', accessToken);
    }
    if (response.data && response.data.hasOwnProperty('code')) {
      if (response.data.code == 401) {
        Message.error('身份已失效');
        setTimeout(() => {
          window.location.href = `http://` + window.location.host
        }, 1000)
      }
    } else {
      setTimeout(() => {
        window.location.href = `http://` + window.location.host
      }, 1000)
    }
    // 统一处理状态
    return response.data;
  },
  // 处理处理
  error => {
    console.log(error);
    if (error && error.response) {
      switch (error.response.status) {
        case 400:
          error.message = `错误请求`;
          break;
        case 401:
          error.message = `未授权,请重新登录`;
          break;
        case 403:
          error.message = `拒绝访问`;
          break;
        case 404:
          error.message = `请求错误,未找到该资源`;
          break;
        case 405:
          error.message = `请求方法未允许`;
          break;
        case 408:
          error.message = `请求超时`;
          break;
        case 500:
          error.message = `服务器端出错`;
          break;
        case 501:
          error.message = `网络未实现`;
          break;
        case 502:
          error.message = `网络错误`;
          break;
        case 503:
          error.message = `服务不可用`;
          break;
        case 504:
          error.message = `网络超时`;
          break;
        case 505:
          error.message = `http版本不支持该请求`;
          break;
        default:
          error.message = `未知错误${error.response.status}`;
      }
    } else {
      error.message = "连接到服务器失败";
      Message.error('连接到服务器失败');
    }
    return Promise.reject(error);
  }
);

/*网络请求部分*/

/*
 *  get请求
 *  url:请求地址
 *  params:参数
 * */

export function get (url, params = {}) {
  return new Promise((resolve, reject) => {
    httpService({
      url: url,
      method: `get`,
      params: params
    })
      .then(response => {
        resolve(response);
      })
      .catch(error => {
        reject(error);
      });
  });
}

/*
 *  post请求
 *  url:请求地址
 *  params:参数
 * */
export function post (url, params = {}) {
  return new Promise((resolve, reject) => {
    httpService({
      url: url,
      method: `post`,
      data: params
    })
      .then(response => {
        resolve(response);
      })
      .catch(error => {
        reject(error);
      });
  });
}

/*
 *  文件上传
 *  url:请求地址
 *  params:参数
 * */
export function fileUpload (url, params = {}) {
  return new Promise((resolve, reject) => {
    httpService({
      url: url,
      method: `post`,
      data: params,
      headers: { "Content-Type": `multipart/form-data` }
    })
      .then(response => {
        resolve(response);
      })
      .catch(error => {
        reject(error);
      });
  });
}

export default {
  get,
  post,
  fileUpload
};

例子的不足

一般情况下,在登录时最好是返回两个token,一个用于在线业务操作token1,一个用于更新token1的token2,token2的时效要比token1长些,在发现token1到时效,还有继续操作的业务时。就将token2利用刷新token的接口发送给后台,拿取最新的两个token。这样做的目的是更显安全,该例子没有那么做,因为目前后台那边是给我一个token参数,然后刷新token的接口无需参数传递也无需验证,所以简化了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值