axios拦截器封装http请求,刷新token重发请求

16,988 阅读4分钟

2020.1.9 更新

之前刷新token后重发请求的代码写得不够成熟,又查询了一些资料,现在把我的代码优化了一下

=============== 手动分割线 ===============

有时候要根据项目的具体需求重新封装http请求。 最近在公司做一个商城项目,因为我也是vue小白,在网上参考了很多资料,才把需求搞定。 以我的项目为例,需求:

  1. 所有对服务器的请求先访问 '/GetMaintenanceState'接口,如果服务器正在维护就拦截请求,跳转到维护页面并显示维护信息;如果服务器正在运行,再发起请求。
  2. 需要登录后发送的请求:(登录时请求接口'Token',将 access_tokenrefresh_token 保存在localStorage),每次请求都要带自定义请求头 Authorization。
  3. access_token 过期后,用 refresh_token 重新请求刷新token,如果refresh_token 过期跳转到登录页面重新获取token。
  4. 因为我们的所有接口除了网络问题,返回的 status 都是200(OK),请求成功 IsSuccesstrue,请求失败 IsSuccessfalse。请求失败会返回响应的错误码 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);
  }
);