demand

Recently, there is a requirement: After the front-end login, the back-end returns a token and its validity period. When the token expires, the back-end requires to use the old token to obtain a new token. The front-end needs to refresh the token painlessly, that is, the user does not feel the request to refresh the token.

Demand analysis

When a user sends a request, the system checks whether the token has expired. If the token has expired, the refreshToken interface is adjusted first. After obtaining a new token, the system continues the request.

The difficulty of this problem is: when multiple requests are made at the same time, but the interface that refreshed the token has not returned, what should be done with the other requests? We will share the process step by step.

Implementation approach

Since the backend returns the validity time of the token, there are two methods:

Method one:

Intercepts each request before the request is initiated and determines whether the token validity period has expired. If the token validity period has expired, the request is suspended and the token is refreshed before the request continues.

Method 2:

Instead of intercepting the data before the request, intercept the data after the return. After the request is sent and the interface returns an expired request, the token is refreshed and retry again.

The two methods are compared

Methods a

  • Advantages: interception before the request, can save the request, save traffic.
  • Disadvantages: Requires the backend to provide an additional token expiration time field; The interception will fail if the local time is tampered with, especially if the local time is slower than the server time.

PS: The token validity time is suggested to be a period of time, similar to the MaxAge of the cache, rather than an absolute time. When the server time is inconsistent with the local time, the absolute time may be incorrect.

Method 2

  • Advantages: No additional token expiration field is required and no time judgment is required.
  • Disadvantages: Will consume more than one request, consumption of traffic.

To sum up, the advantages and disadvantages of method 1 and method 2 are complementary. Method 1 has the risk of verification failure (when the local time is tamper with, of course, the user does not have the time to change the local time), method 2 is simpler, wait for the server has expired and try again, but it will only consume one more request.

In this case, the blogger chose method two.

implementation

Here will use axios to implement, the method is a request before interception, so will use axios. Interceptors. Request. Use () this method;

Method two is request to intercept, so will use axios. Interceptors. Response. Use () method.

Encapsulates the axiOS basic skeleton

First of all, the token in the project exists in localStorage. Request.js basic skeleton:

import axios from 'axios'/ / fromlocalStorage to obtain the tokenfunction getLocalToken () {
    const token = window.localStorage.getItem('token')
    returnToken} // Add one to the instancesetThe Token method is used to dynamically add the latest Token to the header after login and save the Token inlocalStorage in the instance. SetToken = (token) = > {instance. Defaults. Headers ['X-Token'] = token
  window.localStorage.setItem('token'// Create an axios instance const instance = axios.create({baseURL:'/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json'.'X-Token': GetLocalToken () / / headers token}}) / / intercept data returned by the instance. The interceptors. Response. Use (response = > {/ / This is where the token expiry logic is processedreturn response
}, error => {
  return Promise.reject(error)
})

export default instance
Copy the code

This is an encapsulation of the usual Axios instance in a project. When creating the instance, you put the existing local token into the header and export it out for invocation. The next step is to intercept the returned data.

The instance. The interceptors. Response. Use interceptor implementation

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

{code: 1234, message: 'token expired', data: {}}
Copy the code

In my case, the back-end convention indicates that the token has expired when code === 1234 and requires the token to be refreshed.

instance.interceptors.response.use(response => {
  const { code } = response.data
  if(code === 1234) {// Update the token because the token has expiredreturnRefreshToken ().then(res => {// refresh the token successfully, update the latest token to the header, and save it inlocalStorage 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 = ' '// The url is already loaded with/API to avoid the/API/API situation // retry the current request and return the promisereturn instance(config)
    }).catch(res => {
      console.error('refreshtoken error =>', res) // refresh the token failed, the god also can not save, jump to the home page to log in window.location.href ='/'})}return response
}, error => {
  return Promise.reject(error)
})

function refreshToken() {// instance is the current instance of Axios created in request.jsreturn instance.post('/refreshtoken').then(res => res.data)
}
Copy the code

Here, it needs additional attention that response.config is the configuration of the original request, but this has already been processed. Config. url has been loaded with baseUrl, so it needs to be removed in retry, and the token is old, which needs to be refreshed.

This is basically a painless refresh of the token, which is returned when the token is normal, and when the token has expired, axios does an internal refresh and retry of the token. To the caller, the refresh token inside Axios is a black box and is unaware, so the requirement is already done.

Problem and optimization

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

How do I prevent multiple token refreshes

If the refreshToken interface has not returned and another expired request comes in, the above code will execute refreshToken again. This will cause the refreshToken interface to be executed multiple times, so you need to prevent this problem. We can use a flag in request.js to mark whether the token is being refreshed. If the token is being refreshed, the interface for refreshing the token is no longer called.

// Whether the flag is being refreshedlet 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)
})
Copy the code

This will avoid entering the method when the token is refreshed. If two requests are made at the same time, the first one must enter refreshToken and try again, while the second one is discarded and still fails, so you need to resolve the retry issue of the other interfaces.

When two or more requests are made at the same time, how can I retry on other interfaces

The first interface will enter the process of refreshing the token and retry, while the second interface needs to save the token first and retry after refreshing the token. Similarly, if three requests are sent at the same time, cache the last two interfaces and try again after the token is refreshed. Because the interfaces are asynchronous, this can be a bit of a hassle.

When the second expired request comes in and the token is being refreshed, we first store the request in an array queue and try to keep the request waiting until the token is refreshed and then try to clear the request queue one by one. So how do you keep the request waiting? To solve this problem, we had to turn to Promise. We queued the request and returned a Promise, leaving the Promise in the Pending state. The request will wait and wait as long as we do not call resolve. When the refreshed interface returns, we call resolve again and try again one by one. Final code:

// Whether the flag is being refreshedlet isRefreshing = false// Retry queue, each entry will be a function form to be executedlet 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 = ' 'ForEach (cb => cb(token)) // In requests for requests = []return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false})}else{// Refreshing the token to return a promise that has not yet been resolvedreturnNew Promise((resolve) => {// Put the resolve in the queue, save it in a function, and immediately execute requests. Push ((token) => {config.baseurl =' '
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})
Copy the code

The requests queue holds a function for resolve to not execute, so it’s easier to call the function for resolve to execute after the token is refreshed. At this point, the problem should be solved.

Final complete code

import axios from 'axios'/ / fromlocalStorage to obtain the tokenfunction getLocalToken () {
    const token = window.localStorage.getItem('token')
    returnToken} // Add one to the instancesetThe Token method is used to dynamically add the latest Token to the header after login and save the Token inlocalStorage in the instance. SetToken = (token) = > {instance. Defaults. Headers ['X-Token'] = token
  window.localStorage.setItem('token', token)
}

function refreshToken() {// instance is the current instance of Axios created in request.jsreturn 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 Plug token}}) // whether the token is refreshinglet isRefreshing = false// Retry queue, each entry will be a function form to be executedlet 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 = ' 'ForEach (cb => cb(token)) requests = []return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false})}else{// Refreshing the token will return a promise that has not yet been resolvedreturnNew Promise((resolve) => {// Put the resolve in the queue, save it in a function, and immediately execute requests. Push ((token) => {config.baseurl =' '
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

export default instance
Copy the code

I hope that’s helpful. Thanks for seeing the end and liking ^_^.

Subsequent updates

For the implementation of Method 1, please read: How Axios Uses Promises to painlessly refresh tokens (2).