2020.1.9 update

The code of reissuing request after refreshing token was not mature enough, and I queried some data. Now I have optimized my code

=============== Manual dividing line ===============

Sometimes HTTP requests need to be repackaged based on the specific needs of the project. Recently, I was working on a shopping mall project in my company. Since I am also a vUE ignorant, I consulted a lot of information on the Internet before I could complete the requirements. Take my project as an example. Requirements:

  1. All requests to the server first go to the ‘/GetMaintenanceState’ interface. If the server is being maintained, the request is blocked and the maintenance page is displayed. If the server is running, initiate the request again.
  2. Requests that need to be sent after login :(request interface ‘Token’ at login, willaccess_tokenrefresh_tokenStored in localStorage), each request is subject to custom request header Authorization.
  3. access_tokenAfter expiration, userefresh_tokenRerequest to refresh token ifrefresh_tokenExpired Go to the login page to obtain the token again.
  4. Because all of our interfaces except network issues returnstatusBoth are 200(OK), request successfulIsSuccesstrue, the request failedIsSuccessfalse. A response error code is returned if the request failsErrorTypeCode, 10003 –access_tokenNonexistent or expired, 10004 —refresh_tokenDoes not exist or is expired.

Train of thought

There are two types of requests, one that requires a Token and one that does not. I’m going to focus on the first one.

Set up request and Response interceptors. In order to reduce the pressure on the server, obtain the server status first when initiating a request and store it in localStorage. If there is another request within 10 minutes, the server status will not be obtained. Check in the Request interceptor to see if the server is running and has an Access_token. If not, jump to the login page. Most importantly, implementing a refresh token to resend requests when an Access_token expires needs to be set in the Response interceptor.

The server generates a token at two times: Access_token expiration time and refresh_token expiration time. The refresh_token expiration time must be longer than the access_Token expiration time. When the Access_Token expires, you can refresh the token with the refresh_token.

Encapsulates a function that gets the server maintenance state

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, // Whether the server is running
          errMsg: res.data.Value.MaintenanceMsg // Information during maintenance
        };
        // localStorageSet is an encapsulated method that stores fields and time stamps
        localStorageSet('maintenance', status);
        // Pass the result
        return Promise.resolve(status);
      }
    })
    .catch((a)= > {
      return Promise.reject();
    });
}
Copy the code

Encapsulates the function that refreshes the token

function getRefreshToken() {
  let url = getUrl('/Token');
  // The token has been obtained and stored in localStorage during login
  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'.// Developer key
      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));
        // Store access_token in session
        sessionStorage.setItem('access_token', res.data.access_token);
        return Promise.resolve();
      } 
    })
    .catch((a)= > {
      return Promise.reject();
    });
}
Copy the code

Setting up interceptors

Because to encapsulate requests with different requirements, it is best to create an AXIOS instance (mainly the most complex requests here)

Request interceptor:

import router from '.. /router';
import { Message } from 'element-ui';

const instance = axios.create();

instance.interceptors.request.use(
  config= > {
    // Get the local maintenance status of the storage, localStorageGet method, return false after 10 minutes
    let maintenance = localStorageGet('maintenance');
    // If the maintenance file does not exist locally or it takes more than 10 minutes to obtain it, obtain it again
    if(! maintenance) {return checkMaintenance()
        .then(res= > {
          if (res.IsRun) {
          // Get access_token in session
            let access_token = sessionStorage.getItem('access_token');
            // If no field exists, jump to the login page
            if(! access_token) { router.push({path: '/login'.query: { redirect: router.currentRoute.fullPath }
              });
              // Abort the request
              return Promise.reject();
            } else {
              config.headers.Authorization = `bearer ${access_token}`;
            }
            config.headers['Content-Type'] = 'application/json; charset=UTF-8';
            // This step allows the request to be sent
            return config;
          } else {
            // If the server is being maintained, go to the maintenance page to display the maintenance information
            router.push({
              path: '/maintenance'.query: { redirect: res.errMsg }
            });
            return Promise.reject();
          }
        })
        .catch((a)= > {
        // Failed to obtain the server running status
          return Promise.reject();
        });
    } else { // Maintenance exists locally
      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 is an error object, but in my project, it only occurs when there is a network problem
    return Promise.reject(err); });Copy the code

Response interceptor:

This is just for my project, because all requests are successful, separated by the ErrorTypeCode error code, so handled in the Response callback.

In common cases, the token expiration returns error code 10004, which should be handled in err callback.

instance.interceptors.response.use(
  response= > {
    // Access_token does not exist or has expired
    if (response.data.ErrorTypeCode === 10003) {
      const config = response.config
      return getRefreshToken()
        .then((a)= > {
          // Reset
          let access_token = sessionStorage.getItem('access_token');
          config.headers.Authorization = `bearer ${access_token}`;
          config.headers['Content-Type'] = 'application/json; charset=UTF-8';
          // request again
          // Refresh_token also expires when requested
          return instance(config).then(res= > {
            if (res.data.ErrorTypeCode === 10004) {
              router.push({
                path: '/login'.query: { redirect: router.currentRoute.fullPath }
              });
              return Promise.reject();
            }
            // Make the response result omit the data field
            return Promise.resolve(response.data);
          });
        })
        .catch((a)= > {
          // If refreshToken fails to obtain, only the login page is displayed
          router.push({
            path: '/login'.query: { redirect: router.currentRoute.fullPath }
          });
          return Promise.reject();
        });
    }
    // refresh_token does not exist or expires
    if (response.data.ErrorTypeCode == 10004) {
      router.push({
        path: '/login'.query: { redirect: router.currentRoute.fullPath }
      });
      return Promise.reject();
    }
    // Make the response result omit the data field
    return response.data;
  },
  err => {
    return Promise.reject(err); });Copy the code

Encapsulates the request

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((a)= > {
      Message.error('Request failed, please check network connection');
      return Promise.reject();
    });
}
// Expose the member to the outside
export function get(setting) {
  setting.method = 'GET';
  return request(setting);
}

export function post(setting) {
  setting.method = 'POST';
  return request(setting);
}
Copy the code

use

import { post, get } from '@/common/network';

post({
  url: '/api/xxxxx'.Value: {
    GoodsName,
    GoodsTypeId
  }
}).then(res= > {
    / /...
})
Copy the code

The above package is only for the needs of this project, I hope it can help you

Code optimization

How do I prevent the token from being refreshed multiple times

If the refreshToken interface has not yet returned and another expired request comes in, the above code will execute refresh_token again, which will result in the refreshToken interface being executed multiple times, so this problem needs to be prevented. We can use a flag to indicate whether the token state is being refreshed. If the token state is being refreshed, the interface for refreshing the token will not be called.

How do other interfaces retry when two or more requests are made at the same time

Both interfaces initiate and return at almost the same time. The first interface enters the process of refreshing the token and retry, while the second interface needs to be saved and retry after refreshing the token. Similarly, if three requests are made at the same time, you need to cache the last two interfaces and retry after refreshing the tokens. Because the interfaces are asynchronous, it can be a bit cumbersome to process.

When the second expired request comes in and the token is being refreshed, we store the request in an array queue, try to keep the request on hold, wait until the token is refreshed, and then try to clear the queue one by one. So how do you keep the request on hold? To solve this problem, we need to use Promise. After we queue the request, we return a Promise that will remain Pending (resolve will not be called), and the request will wait and wait as long as resolve is not executed. When the refresh request interface returns, we call resolve and retry each one.

The final optimized response interceptor:

// The flag that is being refreshed
let isRefreshing = false;
// Retry queue, each item will be a function to be executed
let requests = [];
instance.interceptors.response.use(
  response= > {
    if (response.data.ErrorTypeCode == 10003) {
      const config = response.config;
      if(! isRefreshing) { isRefreshing =true;
        return getRefreshToken()
          .then((a)= > {
            let access_token = sessionStorage.getItem('access_token');
            config.headers.Authorization = `bearer ${access_token}`;
            config.headers['Content-Type'] = 'application/json; charset=UTF-8';
            // The token has been flushed and all requests in the queue are retried
            requests.forEach(cb= > cb(access_token));
            requests = [];
            return instance(config);
          })
          .catch((a)= > {
            // If refreshToken fails to obtain, only the login page is displayed
            sessionStorageRemove('user');
            router.push({
              path: '/login'.query: { redirect: router.currentRoute.fullPath }
            });
            return Promise.reject();
          })
          .finally((a)= > {
            isRefreshing = false;
          });
      } else {
        // Refreshing tokens will return an unexecuted resolve promise
        return new Promise(resolve= > {
          // Queue resolve, save it as a function, and execute it directly after token refresh
          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); });Copy the code