Several ways for the front-end to realize the senseless refresh of token (pro test is effective)

Reprint https://segmentfault.com/a/1190000020210980

demand

Recently, there is a demand: after the front end logs in, the back end returns the token and the valid time of the token. When the token expires, it is required to use the old token to obtain the new token. The front end needs to refresh the token painlessly, that is, when requesting to refresh the token, the user should be unaware.

Demand analysis

When the user initiates a request, judge whether the token has expired. If it has expired, call the refreshToken interface first, get a 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 interface for refreshing the token has not returned, how should other requests be handled? Next, we will share the whole process step by step

Realization idea

Since the back end returns the valid time of the token, there are two methods:

Method 1:

Before the request is initiated, intercept each request and judge whether the valid time of the token has expired. If it has expired, suspend the request and refresh the token before continuing the request.

Method 2:

Instead of intercepting before the request, the returned data is intercepted. First initiate the request. After the interface returns expired, refresh the token and try again.

Comparison of two methods

Method 1
  • Advantages: intercepting before request can save request and traffic.
  • Disadvantages: the backend needs to provide an additional token expiration time field; The local time judgment is used. If the local time is tampered with, especially when the local time is slower than the server time, the interception will fail.

PS: it is recommended that the effective time of the token be a time period, similar to the MaxAge of the cache, rather than an absolute time. When the server and local time are inconsistent, the absolute time will be problematic.

Method 2
  • Advantages: there is no need for additional token expiration field and no need to judge the time.
  • Disadvantages: it will consume one more request and traffic.

To sum up, the advantages and disadvantages of methods 1 and 2 are complementary. Method 1 has the risk of verification failure (when the local time is tampered with, of course, there is generally no idle egg pain for users to change the local time). Method 2 is simpler and rough. When you know that the server has expired, you will try again, which will only consume one more request.

Here, bloggers choose method 2.

realization

axios will be used here. The first method is to intercept before the request, so axios.interceptors.request.use() will be used;

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

Encapsulating the basic framework of axios
First of all, the token in the project exists in localStorage. request.js basic skeleton:

import axios from 'axios'

// Get token from localStorage
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 localStorage
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 token
  }
})

// Intercept returned data
instance.interceptors.response.use(response => {
  // Next, the logical processing of token expiration will be performed 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 calling. The next step is how to intercept the returned data.

instance.interceptors.response.use interception implementation

The back-end interface generally has an agreed data structure, such as:

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

As I mentioned here, when code === 1234, it means that the token has expired. At this time, it is required to refresh the token.

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // Description the token has expired. Refresh the token
    return refreshToken().then(res => {
      // The token is refreshed successfully. Update the latest token to the header and save it in localStorage
      const { token } = res.data
      instance.setToken(token)
      // Get current failed requests
      const config = response.config
      // Reset the configuration
      config.headers['X-Token'] = token
      config.baseURL = '' // The url has been brought with / api to avoid the situation of / api/api
      // Retry the current request and return promise
      return instance(config)
    }).catch(res => {
      console.error('refreshtoken error =>', res)
      //Failed to refresh the token. The immortal can't 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 the current request.js
    return instance.post('/refreshtoken').then(res => res.data)
}

It should be noted that the response.config is the configuration of the original request, but this has been processed. The config.url has brought the baseUrl, so it needs to be removed when retrying. At the same time, the token is also old and needs to be refreshed.

The above basically achieves the painless refresh of the token. When the token is normal, it returns normally. When the token has expired, axios will refresh the token and try again. For the caller, the refresh token inside axios is a black box and is imperceptible, so the requirement has been met.

Problems and optimization

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

How to prevent multiple refresh of token s

If the refreshToken interface has not returned yet, another expired request will come in, and the above code will execute the refreshToken again, which will lead to the interface that executes the refresh token multiple times. Therefore, this problem needs to be prevented. We can use a flag in request.js to mark whether the token is currently being refreshed. If it is being refreshed, the interface to refresh the token will not be called

// Are you refreshing your tags
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)
})

In this way, you can avoid entering the method again when refreshing the token. However, this approach is equivalent to discarding other failed interfaces. If two requests are initiated at the same time and returned almost at the same time, the first request must enter the refreshToken and retry, while the second request is discarded and still fails to return. Therefore, the retry problem of other interfaces must be solved next.

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

The two interfaces initiate and return almost at the same time. The first interface will enter the process of refreshing the token and retry, while the second interface needs to save it first, and then retry after refreshing the token. Similarly, if three requests are initiated at the same time, the last two interfaces need to be cached, and then try again after refreshing the token. Because the interfaces are asynchronous, it will be a little troublesome to process.

When the second expired request comes in and the token is being refreshed, we first save the request to an array queue and try to keep the request waiting until the token is refreshed, and then try to empty the request queue one by one.
So how do you keep this request waiting? In order to solve this problem, we have to use Promise. After the request is stored in the queue, a Promise is returned at the same time. Keep the Promise in the Pending state (that is, do not call resolve). At this time, the request will wait and wait. As long as we do not execute resolve, the request will wait all the time. When the interface of the refresh request returns, we call resolve and try again one by one. Final code:

// Are you refreshing your tags
let isRefreshing = false
// Retry queue, each item will be in the form of 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 = ''
        // The token has been refreshed and all requests in the queue will be retried
        requests.forEach(cb => cb(token))
        // Don't forget to clear the queue after trying again (students in the Nuggets comment area point out)
        requests = []
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    } else {
      // Refreshing token and returning a promise without resolve
      return new Promise((resolve) => {
        // Put the resolve into the queue, save it in a function form, and execute it directly after the token is refreshed
        requests.push((token) => {
          config.baseURL = ''
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

Change jobs? Looking for interview questions? Come to the front-end interview question bank wx to search the advanced front-end

Keywords: Front-end jenkins Nginx

Added by praveenhotha on Wed, 01 Dec 2021 19:23:10 +0200