现状
项目采用前后端分离开发,前后端使用access_token(即token)进行交互认证,但access_token有一个有效期,在access_token过期后,请求接口将无法成功,现在的处理方式是直接退出跳转至登录入口要求重新登录,但这种方式体验非常不友好,如果当前用户正在录入大量数据时token已经失效,提交数据时直接就退出了,从产品及交互上这种方式是不允许的。
分析
后端采用 IdentityService4
构建认证与授权,在登录成功后除返回access_token之外,增加了expires_in、refresh_token。并增加交换token接口。
expires_in :access_token的过期时间。
refresh_token
:刷新token。access_token过期后可以使用refresh_token交换新的access_token。一个refresh_token只能使用一次。
交换token接口
:使用refresh_token交换access_token,得到新的access_token、新的expires_in、新的refresh_token。
那么前端刷新token即可有两种方式
1、在request请求之前进行拦截,根据expires_in计算出当前token是否过期,若已过期,则将请求挂起,先调用交换token接口,得到新的access_token后再继续请求。
这里本项目放弃此方式。
2、后端接口在检查到access_token过期后,返回状态码40001(前后端约定值),那么在response中进行拦截,当返回状态码为40001时,调用交换token接口,得到新的access_token后再将原请求重发。
实现
对axios进行封装
代码语言:txt复制import axios from 'axios';
代码语言:txt复制import router from '@/router'
代码语言:txt复制import Vue from 'vue'
代码语言:txt复制import { Loading } from 'element-ui';
代码语言:txt复制import qs from 'qs';
代码语言:txt复制let host = window.g.ApiUrl
代码语言:txt复制let loadingInstance; //loading 实例
代码语言:txt复制let needLoadingRequestCount = 0; //当前正在请求的数量
代码语言:txt复制//是否有请求正在刷新token
代码语言:txt复制let isRefreshing = false
代码语言:txt复制// 重试请求队列 每一项都是一个待执行待函数
代码语言:txt复制let requests= [];
代码语言:txt复制//Loading 封装
代码语言:txt复制/*
代码语言:txt复制* 打开全页loading
* this.$showLoading()
* */
Vue.prototype.$showLoading = function(text='加载中...'){
if (needLoadingRequestCount == 0) {
loadingInstance = Loading.service({text: text});
}
needLoadingRequestCount ;
};
/*
* 关闭全页loading
* this.$closeLoading()
* */
Vue.prototype.$closeLoading = function(type=0){
needLoadingRequestCount--;
if(type == 1){
loadingInstance.close();
return false;
}
if (needLoadingRequestCount <= 0) {
loadingInstance.close();
}
}
/**
* 刷新token
*/
function refreshToken (response,instance) {
const refreshtoken = sessionStorage.getItem('refresh_token');
// 判断 没有refresh_token的处理
if (!refreshtoken) {
sessionStorage.removeItem('access_token')
sessionStorage.removeItem('sso_token')
sessionStorage.removeItem('expires_in')
sessionStorage.removeItem('refresh_token')
window.location.href = window.g.mainSiteUrl;//返回登陆
}
let param = {
client_id: window.g.client_id,
client_secret: window.g.client_secret,
grant_type: 'refresh_token',
refresh_token: refreshtoken
};
// instance是当前已创建的axios实例
return instance.post('/connect/token',qs.stringify(param)).then(res => {
//业务系统token
sessionStorage.setItem('access_token', res.access_token);
//业务系统token过期时间
sessionStorage.setItem('expires_in',res.expires_in);
//业务系统refresh_token
sessionStorage.setItem('refresh_token', res.refresh_token);
代码语言:txt复制 // 重新请求接口 前过期的接口
代码语言:txt复制 response.config.headers.Authorization ="Bearer " sessionStorage.getItem('access_token');
代码语言:txt复制 // 已经刷新了token,将所有队列中的请求进行重试,最后再清空队列
代码语言:txt复制 requests.forEach(cb => cb( res.access_token))
代码语言:txt复制 requests = []
代码语言:txt复制 return instance(response.config)
代码语言:txt复制 }).catch(res => {
代码语言:txt复制 sessionStorage.removeItem('access_token')
代码语言:txt复制 sessionStorage.removeItem('sso_token')
代码语言:txt复制 sessionStorage.removeItem('expires_in')
代码语言:txt复制 sessionStorage.removeItem('refresh_token')
代码语言:txt复制 //返回登陆
代码语言:txt复制 window.location.href = window.g.mainSiteUrl;
代码语言:txt复制 }).finally(() => {
代码语言:txt复制 isRefreshing = false
代码语言:txt复制 })
代码语言:txt复制}
代码语言:txt复制export default function $axios(options) {
代码语言:txt复制 return new Promise((resolve, reject) => {
代码语言:txt复制 const instance = axios.create({
代码语言:txt复制 baseURL: host,
代码语言:txt复制 isEditContentType:true,
代码语言:txt复制 isUpload:false
代码语言:txt复制 })
代码语言:txt复制 // request 拦截器
代码语言:txt复制 instance.interceptors.request.use(
代码语言:txt复制 config => {
代码语言:txt复制 Vue.prototype.$showLoading();
代码语言:txt复制 if(config.url!='/connect/token' && config.isEditContentType){
代码语言:txt复制 config.headers["Content-Type"]='application/json;charset=UTF-8'
代码语言:txt复制 }else if(config.url==='/connect/token'){
代码语言:txt复制 config.headers["Content-Type"]='application/x-www-form-urlencoded'
代码语言:txt复制 }
代码语言:txt复制 let token = sessionStorage.getItem('access_token')
代码语言:txt复制 if (token && !config.isUpload) {
代码语言:txt复制 config.headers.Authorization = "Bearer " token
代码语言:txt复制 }
代码语言:txt复制 // 根据请求方法,序列化传来的参数,根据后端需求是否序列化
代码语言:txt复制 if (config.method === 'post') {
代码语言:txt复制 // if (config.data.__proto__ === FormData.prototype
代码语言:txt复制 // || config.url.endsWith('path')
代码语言:txt复制 // || config.url.endsWith('mark')
代码语言:txt复制 // || config.url.endsWith('patchs')
代码语言:txt复制 // ) {
代码语言:txt复制 // } else {
代码语言:txt复制 //config.data = JSON.stringify(config.data)
代码语言:txt复制 // }
代码语言:txt复制 }else if (config.method === 'get') { //get请求增加时间戳
代码语言:txt复制 let url = config.url;
代码语言:txt复制 url.indexOf('?') === -1 ? config.url = url '?_=' (new Date().getTime()) : config.url = url '&_=' (new Date().getTime());
代码语言:txt复制 }
代码语言:txt复制 return config
代码语言:txt复制 },
代码语言:txt复制 error => {
代码语言:txt复制 Vue.prototype.$closeLoading();
代码语言:txt复制 // 请求错误时
代码语言:txt复制 // 1. 判断请求超时
代码语言:txt复制 if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
代码语言:txt复制 // return service.request(originalRequest);// 再重复请求一次
代码语言:txt复制 }
代码语言:txt复制 // 2. 需要重定向到错误页面
代码语言:txt复制 const errorInfo = error.response
代码语言:txt复制 if (errorInfo) {
代码语言:txt复制 error = errorInfo.data // 页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
代码语言:txt复制 const errorStatus = errorInfo.status; // 404 403 500 ...
代码语言:txt复制 router.push({
代码语言:txt复制 path: `/error/${errorStatus}`
代码语言:txt复制 })
代码语言:txt复制 }
代码语言:txt复制 return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
代码语言:txt复制 }
代码语言:txt复制 )
代码语言:txt复制 // response 拦截器
代码语言:txt复制 instance.interceptors.response.use(response => {
代码语言:txt复制 Vue.prototype.$closeLoading();
代码语言:txt复制 const code = response.data.code
代码语言:txt复制 //接口返回token超时
代码语言:txt复制 if (code === "40001") {
代码语言:txt复制 var config = response.config;
代码语言:txt复制 //当前是否有已经在刷新token,防止多次请求刷新token
代码语言:txt复制 if (!isRefreshing) {
代码语言:txt复制 //没有则请求刷新token
代码语言:txt复制 isRefreshing = true
代码语言:txt复制 return refreshToken(response,instance)
代码语言:txt复制 } else {
代码语言:txt复制 // 正在刷新token,加入队列中,将返回一个未执行resolve的promise
代码语言:txt复制 return new Promise((resolve) => {
代码语言:txt复制 // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
代码语言:txt复制 requests.push((token) => {
代码语言:txt复制 config.headers.Authorization ="Bearer " token;
代码语言:txt复制 resolve(instance(config))
代码语言:txt复制 })
代码语言:txt复制 })
代码语言:txt复制 }
代码语言:txt复制 }
代码语言:txt复制 let data;
代码语言:txt复制 if (response.data == undefined) {
代码语言:txt复制 data = JSON.parse(response.request.responseText)
代码语言:txt复制 } else {
代码语言:txt复制 data = response.data
代码语言:txt复制 }
代码语言:txt复制 return data
代码语言:txt复制 }, error => {
代码语言:txt复制 return Promise.reject(error)
代码语言:txt复制 })
代码语言:txt复制 // 请求处理
代码语言:txt复制 instance(options).then(res => {
代码语言:txt复制 resolve(res)
代码语言:txt复制 return false
代码语言:txt复制 }).catch(error => {
代码语言:txt复制 reject(error)
代码语言:txt复制 })
代码语言:txt复制 })
代码语言:txt复制}