preface


❝ recently, our boss let xiaobai do login module, login module said simple and simple, complex and complex. This chapter describes a series of things that happen when the token is refreshed after the session expires. ❞

demand

In a page, when the request fails and 302 is returned, determine whether the interface is expired or the login is expired. If the interface is expired, request a new token, and then use the new token to initiate the request again.

Graph TD interface 1 --> Request failed 302 --> Login expired --> Re-login interface 2 --> Request failed 302 --> Session expired --> Refresh token --> New Token --> Re-request failed interface 3 --> graph TD interface 1 --> Request failed 302 --> Session expired --> Refresh Token --> New Token --> Re-request failed interface 3 --> Request failed 302

Train of thought


  • At first, I thought of a black technology (for laziness), is to get a new token, directly forced to refresh the page, so that the interface in a page automatically refresh ~ (convenient is convenient, but the user experience is not good).
  • Currently, the idea of re-requesting the interface can be combined with a subscription publishing model to improve the user experience

The response to intercept

First of all, we launched a request axios ({url: ‘/ test’ data: XXX}), then (res = > {})

After intercepting 302, we enter the refresh token logic

Response interception code

axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) { 
            returnresponse; }},(err) = > {
        / / refresh token
        let res = err.response || {}; 
        if(res.data.meta? .statusCode ==302) {
            return refeshToken(res);
        } else {  
            returnerr; }});Copy the code

Our background data format is to determine the expiration according to the statusCode (you can determine your own situation), then go to the refrshToken method ~

Refresh token method

// Avoid simultaneous requests from other interfaces (request token interfaces only once)
let isRefreshToken = false;
const refeshToken = (response) = > {
   if(! isRefreshToken) { isRefreshToken =true;
            axios({
                // Get a new token interface
                url: `/api/refreshToken`,
            })
                .then((res) = > {
                    const { data = ' ', meta = {} } = res.data;
                    if (meta.statusCode === 200) {
                        isRefreshToken = false; 
                        // Publish the message
                        retryOldRequest.trigger(data);
                    } else { 
                        history.push('/user/login');
                    }
                })
                .catch((err) = > { 
                    history.push('/user/login');
                });
        }
        // Collect subscribers and return successful data to the original interface
        return retryOldRequest.listen(response);
};
Copy the code

Some people are a little bit surprised by this retryOldRequest what is this? Yes, this is our male 2 subscription publishing mode queue.

Subscription publishing model


For those of you who are not familiar with the subscription model, you can check it out for an easy to understand example written by The great God (please like it if you think you learned it).

Take the failed interface as a subscriber and publish it after successfully getting a new token (rerequest the interface).

Here is the code for the subscription publishing model

const retryOldRequest = {
    // Maintain the response of the failed request
    requestQuery: [].// Add subscribers
    listen(response) {
        return new Promise((resolve) = > {
            this.requestQuery.push((newToken) = > { 
                let config = response.config || {};
                //Authorization is an identity token passed to the background
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    // Publish the message
    trigger(newToken) {
        this.requestQuery.forEach((fn) = > {
            fn(newToken);
        });
        this.requestQuery = []; }};Copy the code

You can forget about the logic of the subscriber and just know that the subscriber is the reponse after each failed request.

Each time we enter the refeshToken method, our failed interface triggers retryoldrequest.listen to subscribe, and our requestQuery is the queue that holds those subscribers.

Note: Our subscriber queue requestQuery is the method to save to publish. The Retryoldrequest.trigger will publish these messages (new tokens) to subscribers (methods that trigger subscription queues) after the new token has been successfully obtained.

The subscriber (Response) contains config configuration. After we get the new token (after publishing), we modify the request header Autorzation in config. With the help of Promise, we can better get the interface data requested by the new token. Once the requested data is received, We can return the original interface /test unchanged (because refreshToken is returned in the response intercept, which in turn returns the data returned by the subscriber Retryoldrequest. listen, and Listiner returns a Promise Promise resolves after a successful request.

See this, friends do not feel a little round ~

In real development, our logic also includes login expiration (as opposed to request expiration). We determine whether the request is expired or the login is expired according to the current time – past time < expiresTime (epiresTime: return valid time after login). Here is the full logic

Graph TD = expiresTime = expiresTime = expiresTime = expiresTime = expiresTime Interface 2 --> Request failed 302 --> Check by expiresTime --> Session expired --> Refresh token --> New token --> Request failed 302 --> Request failed 302

Here is the complete code

const retryOldRequest = {
    // Maintain the response of the failed request
    requestQuery: [].// Add subscribers
    listen(response) {
        return new Promise((resolve) = > {
            this.requestQuery.push((newToken) = > { 
                let config = response.config || {};
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    // Publish the message
    trigger(newToken) {
        this.requestQuery.forEach((fn) = > {
            fn(newToken);
        });
        this.requestQuery = []; }};/** * sessionExpiredTips * The user is not authorized, and the page is relogged to the login page. * The interface expires => Refresh the token. * Login expires => Re-log in

// Avoid simultaneous requests from other interfaces
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) = > {
    // The validity period obtained after login
    let userExpir = localStorage.getItem('expiresTime');
    // The current time
    let nowTime = Math.floor(new Date().getTime() / 1000);
    // The last requested time
    let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
    let token = localStorage.getItem('token');

    if (token && nowTime - lastResTime < userExpir) {
        if(! isRefreshToken) { isRefreshToken =true;
            axios({
                url: `/api/refreshToken`,
            })
                .then((res) = > {
                    const { data = ' ', meta = {} } = res.data;
                    isRefreshToken = false;
                    if (meta.statusCode === 200) {
                        localStorage.getItem('token', data);
                        localStorage.getItem('lastResponseTime'.Math.floor(new Date().getTime() / 1000));// Publish the message
                        retryOldRequest.trigger(data);
                    } else {
                       / / to login
                    }
                })
                .catch((err) = > {
                    isRefreshToken = false;
                   / / to login
                });
        }
        // Collect subscribers and return successful data to the original interface
        return retryOldRequest.listen(response);
    } else {
        // Throttling: avoid repeated runs
       / / to login}};// HTTP response Response interception
axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) {
            // Record the last operation time
           localStorage.getItem('lastResponseTime'.Math.floor(new Date().getTime() / 1000));
            returnresponse; }},(err) = > { 
        let res = err.response || {}; 
        if(res.data.meta? .statusCode ==302) {
            return refeshToken(res);
        } else {
            // Error not reported by 302;
            returnerr; }});Copy the code

The above is our business, if the writing is not good, please excuse me ~~

If you have a good idea, you can also discuss it in the comments section