Перехватчик axios инкапсулирует http-запрос, обновляет токен и повторно отправляет запрос.

axios

2020.1.9 Обновление

Код для повторной отправки запроса после обновления токена не был достаточно зрелым, и я запросил некоторую информацию, и теперь я оптимизировал свой код.

============== Ручное разделение строк ===============

Иногда необходимо переупаковать http-запросы в соответствии с конкретными потребностями проекта. Недавно я работал над проектом торгового центра в компании, но поскольку я тоже новичок в Vue, я просмотрел много информации в Интернете, прежде чем получил спрос. Возьмите мой проект в качестве примера, требования:

  1. Все запросы к серверу сначала обращаются к интерфейсу «/GetMaintenanceState». Если сервер находится на обслуживании, перехватите запрос, перейдите на страницу обслуживания и отобразите информацию об обслуживании; если сервер работает, инициируйте запрос снова.
  2. Запрос, отправленный после входа в систему, обязателен: (интерфейс «Токен» запрашивается при входе в систему, аaccess_tokenа такжеrefresh_tokenСохраняется в localStorage), каждый запрос должен иметь собственный заголовок запроса Authorization.
  3. access_tokenПо истечении срока использоватьrefresh_tokenПовторный запрос на обновление токена, еслиrefresh_tokenПо истечении срока действия перейдите на страницу входа, чтобы снова получить токен.
  4. Поскольку все наши интерфейсы, кроме проблем с сетью, возвращены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);
  }
);