2020.1.9 Обновление
Код для повторной отправки запроса после обновления токена не был достаточно зрелым, и я запросил некоторую информацию, и теперь я оптимизировал свой код.
============== Ручное разделение строк ===============
Иногда необходимо переупаковать http-запросы в соответствии с конкретными потребностями проекта. Недавно я работал над проектом торгового центра в компании, но поскольку я тоже новичок в Vue, я просмотрел много информации в Интернете, прежде чем получил спрос. Возьмите мой проект в качестве примера, требования:
- Все запросы к серверу сначала обращаются к интерфейсу «/GetMaintenanceState». Если сервер находится на обслуживании, перехватите запрос, перейдите на страницу обслуживания и отобразите информацию об обслуживании; если сервер работает, инициируйте запрос снова.
- Запрос, отправленный после входа в систему, обязателен: (интерфейс «Токен» запрашивается при входе в систему, а
access_token
а такжеrefresh_token
Сохраняется в localStorage), каждый запрос должен иметь собственный заголовок запроса Authorization. -
access_token
По истечении срока использоватьrefresh_token
Повторный запрос на обновление токена, еслиrefresh_token
По истечении срока действия перейдите на страницу входа, чтобы снова получить токен. - Поскольку все наши интерфейсы, кроме проблем с сетью, возвращены
status
Оба 200 (ОК), запрос выполнен успешноIsSuccess
дляtrue
, Запрос не выполненIsSuccess
дляfalse
. Если запрос не выполнен, будет возвращен код ошибки ответа.ErrorTypeCode
, 10003 ——access_token
Отсутствует или просрочен, 10004—refresh_token
не существует или истекает.
идеи
Существует два типа запросов: для одного требуется Token, а для другого Token не требуется. Здесь в основном обсуждается первый.
Установите перехватчики запросов и ответов. Чтобы уменьшить нагрузку на сервер, когда сервер инициировал запрос на получение статуса, хранящегося в localStorage, в течение 10 минут, если другой запрос получает статус дольше.
Определение того, запущен ли на сервере перехватчик запросов, есть лиaccess_token
, если нет, перейдите на страницу входа.
Главное, добитьсяaccess_token
Когда он истечет, обновите токен и повторно отправьте запрос, который необходимо установить в перехватчике ответа.
В процессе генерации токена сервером будет два раза, один - время истечения срока действия токена (access_token
время истечения срока действия), один — время обновления токена (refresh_token
Срок действия).refresh_token
Срок годности однозначноaccess_token
Срок годности больше, когдаaccess_token
По истечении срока можно использоватьrefresh_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();
});
}
Инкапсулирует функцию для обновления маркера
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 (здесь в основном самый сложный запрос)
перехватчик запросов:
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);
}
);
перехватчик ответа:
Это как раз для случая моего проекта, потому что все запросы успешны и отличаются ErrorTypeCode, поэтому обрабатываются в обратном вызове ответа.
В обычных случаях срок действия токена истекает и возвращается код ошибки 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 => {
//.....
})
Вышеупомянутый пакет предназначен только для нужд этого проекта, я надеюсь, что он может вам помочь.
Оптимизация кода
Как предотвратить несколько токенов обновления
Если интерфейс refreshToken не вернулся, а в это время приходит другой запрос с истекшим сроком действия, приведенный выше код будет выполнен снова.refresh_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);
}
);