2020.1.9 更新
之前刷新token后重发请求的代码写得不够成熟,又查询了一些资料,现在把我的代码优化了一下
=============== 手动分割线 ===============
有时候要根据项目的具体需求重新封装http请求。 最近在公司做一个商城项目,因为我也是vue小白,在网上参考了很多资料,才把需求搞定。 以我的项目为例,需求:
- 所有对服务器的请求先访问 '/GetMaintenanceState'接口,如果服务器正在维护就拦截请求,跳转到维护页面并显示维护信息;如果服务器正在运行,再发起请求。
- 需要登录后发送的请求:(登录时请求接口'Token',将
access_token
和refresh_token
保存在localStorage),每次请求都要带自定义请求头 Authorization。 access_token
过期后,用refresh_token
重新请求刷新token,如果refresh_token
过期跳转到登录页面重新获取token。- 因为我们的所有接口除了网络问题,返回的
status
都是200(OK),请求成功IsSuccess
为true
,请求失败IsSuccess
为false
。请求失败会返回响应的错误码ErrorTypeCode
,10003 ——access_token
不存在或过期,10004 ——refresh_token
不存在或过期。
思路
有两种请求,一种需要Token,一种不需要Token。这里主要讲第一种。
设置 request 和 response 拦截器。为了减轻服务器的压力,发起请求的时候先获取服务器状态,储存在 localStorage,10分钟内如果再有请求,不再获取状态。
在request 拦截器中检测服务器是否运行,是否有 access_token
,没有就跳转到登录页面。
最重要的是,实现 access_token
过期时,刷新token重发请求,这个需要在 response 拦截器中设置。
服务器生成 token 的过程中,会有两个时间,一个是 token 失效时间(access_token
过期时间),一个是 token 刷新时间(refresh_token
过期时间)。refresh_token
过期时间肯定比 access_token
过期时间要长,当 access_token
过期时,可以用 refresh_token
刷新 token。
封装获取服务器维护状态的函数
import axios from 'axios';
function getUrl(url) {
if (url.indexOf(baseUrl) === 0) {
return url;
}
url = url.replace(/^\//, '');
url = baseUrl + '/' + url;
return url;
}
function checkMaintenance() {
let status = {};
let url = getUrl('/GetMaintenanceState');
return axios({
url,
method: 'get'
})
.then(res => {
if (res.data.IsSuccess) {
status = {
IsRun: res.data.Value.IsRun, // 服务器是否运行
errMsg: res.data.Value.MaintenanceMsg // 维护时的信息
};
// localStorageSet 为封装好的方法,储存字段的同时,储存时间戳
localStorageSet('maintenance', status);
// 传递获取的结果
return Promise.resolve(status);
}
})
.catch(() => {
return Promise.reject();
});
}
封装刷新token的函数
function getRefreshToken() {
let url = getUrl('/Token');
// 登录时已经获取token储存在localStorage中
let token = JSON.parse(localStorage.getItem('token'));
return axios({
url,
method: 'post',
data: 'grant_type=refresh_token&refresh_token=' + token.refresh_token,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
// 开发者密钥
Authorization: 'Basic xxxxxxxxxxx'
}
})
.then(res => {
if (res.data.IsSuccess) {
var token_temp = {
access_token: res.data.access_token,
refresh_token: res.data.refresh_token
};
localStorage.setItem('token', JSON.stringify(token_temp));
// 将access_token储存在session中
sessionStorage.setItem('access_token', res.data.access_token);
return Promise.resolve();
}
})
.catch(() => {
return Promise.reject();
});
}
设置拦截器
因为要封装不同需求的请求,最好创建 axios 实例(这里主要是最复杂的请求)
request 拦截器:
import router from '../router';
import { Message } from 'element-ui';
const instance = axios.create();
instance.interceptors.request.use(
config => {
// 获取储存中本地的维护状态,localStorageGet方法,超过10分钟返回false
let maintenance = localStorageGet('maintenance');
// 如果本地不存在 maintenance 或 获取超过10分钟,重新获取
if (!maintenance) {
return checkMaintenance()
.then(res => {
if (res.IsRun) {
// 获取session中的access_token
let access_token = sessionStorage.getItem('access_token');
// 如果不存在字段,则跳转到登录页面
if (!access_token) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
// 终止这个请求
return Promise.reject();
} else {
config.headers.Authorization = `bearer ${access_token}`;
}
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
// 这一步就是允许发送请求
return config;
} else {
// 如果服务器正在维护,跳转到维护页面,显示维护信息
router.push({
path: '/maintenance',
query: { redirect: res.errMsg }
});
return Promise.reject();
}
})
.catch(() => {
// 获取服务器运行状态失败
return Promise.reject();
});
} else { // 本地存在 maintenance
if (maintenance.IsRun) {
let access_token = sessionStorage.getItem('access_token');
if (!access_token) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
} else {
config.headers.Authorization = `bearer ${access_token}`;
}
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
return config;
} else {
router.push({
path: '/maintenance',
query: { redirect: maintenance.errMsg }
});
return Promise.reject();
}
}
},
err => {
// err为错误对象,但是在我的项目中,除非网络问题才会出现
return Promise.reject(err);
}
);
response 拦截器:
这只是针对我这个项目的情况,因为所有请求都是成功的,靠ErrorTypeCode错误码区分,所以在response回调中处理。
若是普通情况,token 过期返回错误码 10004,应该中err回调中处理。
instance.interceptors.response.use(
response => {
// access_token不存在或过期
if (response.data.ErrorTypeCode === 10003) {
const config = response.config
return getRefreshToken()
.then(() => {
// 重新设置
let access_token = sessionStorage.getItem('access_token');
config.headers.Authorization = `bearer ${access_token}`;
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
// 重新请求
// 如果请求的时候refresh_token也过期
return instance(config).then(res => {
if (res.data.ErrorTypeCode === 10004) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
}
// 使响应结果省略data字段
return Promise.resolve(response.data);
});
})
.catch(() => {
// refreshtoken 获取失败就只能到登录页面
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
});
}
// refresh_token不存在或过期
if (response.data.ErrorTypeCode == 10004) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
}
// 使响应结果省略data字段
return response.data;
},
err => {
return Promise.reject(err);
}
);
封装请求
function request({ url, method, Value = null }) {
url = getUrl(url);
method = method.toLowerCase() || 'get';
let obj = {
method,
url
};
if (Value !== null) {
if (method === 'get') {
obj.params = { Value };
} else {
obj.data = { Value };
}
}
return instance(obj)
.then(res => {
return Promise.resolve(res);
})
.catch(() => {
Message.error('请求失败,请检查网络连接');
return Promise.reject();
});
}
// 向外暴露成员
export function get(setting) {
setting.method = 'GET';
return request(setting);
}
export function post(setting) {
setting.method = 'POST';
return request(setting);
}
使用
import { post, get } from '@/common/network';
post({
url: '/api/xxxxx',
Value: {
GoodsName,
GoodsTypeId
}
}).then(res => {
//.....
})
以上封装只是针对这个项目的需求,希望能对你有所帮助
代码的优化
如何防止多次刷新token
如果refreshToken接口还没返回,此时再有一个过期的请求进来,上面的代码就会再一次执行 refresh_token
,这就会导致多次执行刷新 token 的接口,因此需要防止这个问题。我们可以用一个 flag 来标记当前是否正在刷新 token 的状态,如果正在刷新则不再调用刷新 token 的接口。
同时发起两个或以上的请求时,其他接口如何重试
两个接口几乎同时发起和返回,第一个接口会进入刷新 token 后重试的流程,而第二个接口需要先存起来,然后等刷新 token 后再重试。同样,如果同时发起三个请求,此时需要缓存后两个接口,等刷新 token 后再重试。由于接口都是异步的,处理起来会有点麻烦。
当第二个过期的请求进来,token 正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。
那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助 Promise
。将请求存进队列中后,同时返回一个 Promise
,让这个 Promise
一直处于 Pending
状态(即不调用 resolve
),此时这个请求就会一直等啊等,只要我们不执行 resolve
,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用 resolve
,逐个重试。
最终优化的响应拦截器:
// 是否正在刷新的标记
let isRefreshing = false;
// 重试队列,每一项将是一个待执行的函数形式
let requests = [];
instance.interceptors.response.use(
response => {
if (response.data.ErrorTypeCode == 10003) {
const config = response.config;
if (!isRefreshing) {
isRefreshing = true;
return getRefreshToken()
.then(() => {
let access_token = sessionStorage.getItem('access_token');
config.headers.Authorization = `bearer ${access_token}`;
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
// 已经刷新了token,将所有队列中的请求进行重试
requests.forEach(cb => cb(access_token));
requests = [];
return instance(config);
})
.catch(() => {
// refreshtoken 获取失败就只能到登录页面
sessionStorageRemove('user');
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
})
.finally(() => {
isRefreshing = false;
});
} else {
// 正在刷新token,将返回一个未执行resolve的promise
return new Promise(resolve => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push(token => {
config.headers.Authorization = `bearer ${token}`;
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
resolve(instance(config));
});
});
}
}
if (response.data.ErrorTypeCode == 10004) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
return Promise.reject();
}
return response.data;
},
err => {
return Promise.reject(err);
}
);