How axios uses promise to refresh token painlessly

Keywords: Javascript axios JSON

demand

Recently, we met a requirement: after the front-end login, the back-end returns token and token valid time. When the token expires, we need to use the old token to get a new token. The front-end needs to refresh token painlessly, that is to say, when requesting to refresh token, it needs to be user-insensitive.

Demand Analysis

When a user initiates a request, judge whether the token has expired. If it has expired, first adjust the refreshToken interface, get the new token, and then continue to execute the previous request.

The difficulty of this problem is: when multiple requests are initiated at the same time and the refreshed token interface has not returned, how can other requests be handled? Next, we will share the whole process step by step.

Ideas for Realization

Since the back end returns token's valid time, there are two ways to do this:

Method 1:

Intercept each request before the request is launched to determine whether the valid time of token has expired. If it has expired, the request will be suspended, refresh the token first and then continue the request.

Method 2:

Instead of intercepting before the request, it intercepts the returned data. Initiate the request first, after the interface returns expired, refresh token first, and then try again.

Comparison of two methods

Method 1

  • Advantages: Intercepting before requests can save requests and traffic.
  • Disadvantage: The back end needs to provide an additional token expiration time field; local time judgment is used, and if local time is tampered with, especially when local time is slower than server time, interception fails.

PS: The token validity time recommendation is a period of time, similar to cached MaxAge, rather than absolute time. Absolute time can be problematic when the server and local time are inconsistent.

Method 2

  • Advantages: No additional token expiration fields and no time judgment are required.
  • Disadvantage: It consumes multiple requests and traffic.

To sum up, the advantages and disadvantages of methods one and two are complementary. Method one has the risk of failure of verification (when local time is tampered with, of course, there is no pain in the user's spare eggs to change local time). Method two is more simple and rude, and it will only consume one more request when it knows that the server has expired and retries.

Here the blogger chose the second method.

Realization

It will be used here. axios First, intercept before request, so axios.interceptors.request.use() is used.

The second method is post-request interception, so the axios.interceptors.response.use() method is used.

Packaging axios basic skeleton

First, let's say that token in the project exists in the local Storage. Requ.js basic skeleton:

import axios from 'axios'

// Getting token from local Storage
function getLocalToken () {
    const token = window.localStorage.getItem('token')
    return token
}


// Add a setToken method to the instance to dynamically add the latest token to the header after login, and save the token in the local Storage
instance.setToken = (token) => {
  instance.defaults.headers['X-Token'] = token
  window.localStorage.setItem('token', token)
}

// Create an axios instance
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Token': getLocalToken() // headers settoken
  }
})

// Intercept returned data
instance.interceptors.response.use(response => {
  // Next, we will do the logical processing of token expiration here.
  return response
}, error => {
  return Promise.reject(error)
})

export default instance

This is the encapsulation of the general axios instance in the project. When creating the instance, put the local existing token into the header, and then export it for invocation. Next is how to intercept the returned data.

Implementation of instance.interceptors.response.use interception

Back-end interfaces generally have a well-defined data structure, such as:

{code: 1234, message: 'token Be overdue', data: {}}

As I see here, the back-end convention indicates that token expires when code == 1234, at which point token is required to be refreshed.

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // Explain that token expires, refresh token
    return refreshToken().then(res => {
      // Refresh token successfully, update the latest token to header and save it in local Storage
      const { token } = res.data
      instance.setToken(token)
      // Get the current failed request
      const config = response.config
      // Reset the configuration
      config.headers['X-Token'] = token
      config.baseURL = '' // url has been brought with / api to avoid / api/api situations
      // Retry the current request and return promise
      return instance(config)
    }).catch(res => {
      console.error('refreshtoken error =>', res)
      //Refreshing token failed and the immortal could not save it. Jump to the home page and log in again.
      window.location.href = '/'
    })
  }
  return response
}, error => {
  return Promise.reject(error)
})

function refreshToken () {
    // Instance is the axios instance created in current request.js
    return instance.post('/refreshtoken').then(res => res.data)
}

Additionally, response.config is the configuration of the original request, but this has already been processed. config.url has been brought with baseUrl, so it needs to be removed when retrying, and token is old and needs to be refreshed.

The above basically achieves painless refresh token, when token is normal, return to normal, when token has expired, then axios internal refresh token and retry. For the caller, the refresh token inside axios is a black box and is imperceptible, so the requirement has been met.

Problem and optimization

There are still some problems with the above code, which does not take into account the problem of multiple requests, so it needs to be further optimized.

How to prevent multiple token refreshes

If the refreshToken interface hasn't returned yet, then another expired request comes in, and the above code will execute refreshToken again, which will result in multiple executions of the refresh token interface, so you need to prevent this problem. We can use a flag in request.js to mark whether token is currently being refreshed, and if it is being refreshed, we can no longer call the refreshed token interface.

// Is the marker being refreshed?
let isRefreshing = false
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        const config = response.config
        config.headers['X-Token'] = token
        config.baseURL = ''
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

This avoids re-entering the method when token is refreshed. But this approach is equivalent to abandoning the other failed interfaces. If two requests are initiated at the same time and returned almost simultaneously, the first request must be retried after entering refreshToken, while the second request is discarded, which still fails to return, so the retry problem of other interfaces has to be solved next.

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

Both interfaces initiate and return almost simultaneously. The first interface enters the process of refreshing token and retrying, while the second interface needs to be saved first, and then retried after refreshing token. Similarly, if three requests are made at the same time, the two interfaces after the cache need to be cached and retried after the token is refreshed. Because the interfaces are asynchronous, it's a bit cumbersome to handle.

When the second expired request comes in, token is refreshing. First, we save the request in an array queue and try to keep the request waiting until the token is refreshed. Then we try to empty the request queue one by one.
So how do you keep the request waiting? To solve this problem, we have to use Promise. When the request is put in the queue and a Promise is returned, the Promise is kept in the Pending state (that is, no resolution is called), and the request will wait and wait as long as we do not execute the resolution, the request will be waiting all the time. When the interface for the refresh request returns, we call resolve and try again one by one. The final code:

// Is the marker being refreshed?
let isRefreshing = false
// Retry queue, each item will be a function to be executed
let requests = []

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    const config = response.config
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        config.headers['X-Token'] = token
        config.baseURL = ''
        // token has been refreshed to retry requests from all queues
        requests.forEach(cb => cb(token))
        // Don't forget to clear the queue after the retry.
        requests = []
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    } else {
      // Refreshing token, returning a promise that did not perform resolve
      return new Promise((resolve) => {
        // Put resolve in the queue, save it in a function form, and execute it directly after token refreshes
        requests.push((token) => {
          config.baseURL = ''
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

What may be difficult to understand here is that the request queue holds a function in order to keep the resolve from executing, save it first, and call it more easily after refreshing the token so that the resolve can execute. So far, all the problems should be solved.

Last complete code

import axios from 'axios'

// Getting token from local Storage
function getLocalToken () {
    const token = window.localStorage.getItem('token')
    return token
}

// Add a setToken method to the instance to dynamically add the latest token to the header after login, and save the token in the local Storage
instance.setToken = (token) => {
  instance.defaults.headers['X-Token'] = token
  window.localStorage.setItem('token', token)
}

function refreshToken () {
    // Instance is the axios instance created in current request.js
    return instance.post('/refreshtoken').then(res => res.data)
}

// Create an axios instance
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Token': getLocalToken() // headers settoken
  }
})

// Is the marker being refreshed?
let isRefreshing = false
// Retry queue, each item will be a function to be executed
let requests = []

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    const config = response.config
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        config.headers['X-Token'] = token
        config.baseURL = ''
        // token has been refreshed to retry requests from all queues
        requests.forEach(cb => cb(token))
        requests = []
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    } else {
      // token is being refreshed and a promise that resolve has not been executed is returned
      return new Promise((resolve) => {
        // Put resolve in the queue, save it in a function form, and execute it directly after token refreshes
        requests.push((token) => {
          config.baseURL = ''
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

export default instance

I hope it will be helpful to you all. Thank you for seeing the end. Thank you for your compliments.

Posted by bostonmacosx on Tue, 27 Aug 2019 22:11:23 -0700