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.