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的状态
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的接口无需参数传递也无需验证,所以简化了。