會話過期後token刷新,重新請求接口(訂閲發佈模式)

語言: CN / TW / HK

 

需求

在一個頁面內,當請求失敗並且返回 302 後,判斷是接口過期還是登錄過期,如果是接口過期,則去請求新的token,然後拿新的token去再次發起請求.

 

思路


  • 當初,想了一個黑科技(為了偷懶),就是拿到新的token後,直接強制刷新頁面,這樣一個頁面內的接口就自動刷新啦~(方便是方便,用户體驗卻不好)
  • 目前,想到了重新請求接口時,可以配合訂閲發佈模式來提高用户體驗

響應攔截

首先我們發起一個請求 axios({url:'/test',data:xxx}).then(res=>{})

攔截到302後,我們進入到刷新token邏輯

響應攔截代碼

axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) { 
            return response;
        }
    },
    (err) => {
        //刷新token
        let res = err.response || {}; 
        if (res.data.meta?.statusCode == 302) {
            return refeshToken(res);
        } else {  
            return err;
        }
    }
);
複製代碼

我們後台的數據格式是根據statusCode來判斷過期(你們可以根據自己的實際情況判斷),接着進入refrshToken方法~

刷新token方法

//避免其他接口同時請求(只請求一次token接口)
let isRefreshToken = false;
const refeshToken = (response) => {
   if (!isRefreshToken) {
            isRefreshToken = true;
            axios({
                //獲取新token接口
                url: `/api/refreshToken`,
            })
                .then((res) => {
                    const { data = '', meta = {} } = res.data;
                    if (meta.statusCode === 200) {
                        isRefreshToken = false; 
                        //發佈 消息
                        retryOldRequest.trigger(data);
                    } else { 
                        history.push('/user/login');
                    }
                })
                .catch((err) => { 
                    history.push('/user/login');
                });
        }
        //收集訂閲者 並把成功後的數據返回原接口
        return retryOldRequest.listen(response);
};
複製代碼

看到這,有的小夥伴就有點奇怪retryOldRequest這個又是什麼?沒錯,這就是我們男二 訂閲發佈模式隊列。

訂閲發佈模式

把失敗的接口當訂閲者,成功拿到新的token後再發布(重新請求接口)。

以下便是訂閲發佈模式代碼

const retryOldRequest = {
    //維護失敗請求的response
    requestQuery: [],

    //添加訂閲者
    listen(response) {
        return new Promise((resolve) => {
            this.requestQuery.push((newToken) => { 
                let config = response.config || {};
                //Authorization是傳給後台的身份令牌
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    //發佈消息
    trigger(newToken) {
        this.requestQuery.forEach((fn) => {
            fn(newToken);
        });
        this.requestQuery = [];
    },
};
複製代碼

大家可以先不用關注訂閲者的邏輯,只需要知道訂閲者是每次請求失敗後的接口(reponse)就好了。

每次進入refeshToken方法,我們失敗的接口都會觸發retryOldRequest.listen去訂閲,而我們的requestQuery則是保存這些訂閲者的隊列。

注意:我們訂閲者隊列requestQuery是保存待發布的方法。而在成功獲取新token後,retryOldRequest.trigger就會去發佈這些消息(新token)給訂閲者(觸發訂閲隊列的方法)。

而訂閲者(response)裏面有config配置,我們拿到新的token後(發佈後),修改config裏面的請求頭Autorzation.而藉助Promise我們可以更好的拿到新token請求回來的接口數據,一旦請求到數據,我們可以原封不動的返回給原來的接口/test了(因為我們在響應攔截那裏返回的是refreshToken,而refreshToken又返回的是訂閲者retryOldRequest.listen返回的數據,而Listiner又返回Promise的數據,Promise又在成功請求後resolve出去)。

看到這,小夥伴們是不是覺得有點繞了~

而在真實開發中,我們的邏輯還含有登錄過期(與請求過期區分開來)。我們是根據 當前時間 - 過去時間 < expiresTime(epiresTime:登錄後返回的有效時間)來判斷是請求過期還是登錄過期的。 以下是完整邏輯

以下是完整代碼

const retryOldRequest = {
    //維護失敗請求的response
    requestQuery: [],

    //添加訂閲者
    listen(response) {
        return new Promise((resolve) => {
            this.requestQuery.push((newToken) => { 
                let config = response.config || {};
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    //發佈消息
    trigger(newToken) {
        this.requestQuery.forEach((fn) => {
            fn(newToken);
        });
        this.requestQuery = [];
    },
};
/**
 * sessionExpiredTips
 * 會話過期:
 * 刷新token失敗,得重新登錄
 * 用户未授權,頁面跳轉到登錄頁面 
 * 接口過期 => 刷新token
 * 登錄過期 => 重新登錄
 * expiresTime => 在本業務中返回18000ms == 5h
 * ****/

//避免其他接口同時請求
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) => {
    //登錄後拿到的有效期
    let userExpir = localStorage.getItem('expiresTime');
    //當前時間
    let nowTime = Math.floor(new Date().getTime() / 1000);
    //最後請求的時間
    let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
    //登錄後保存到本地的token
    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.setItem('token', data);
                        localStorage.setItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                        );
                        //發佈 消息
                        retryOldRequest.trigger(data);
                    } else {
                       //去登錄
                    }
                })
                .catch((err) => {
                    isRefreshToken = false;
                   //去登錄
                });
        }
        //收集訂閲者 並把成功後的數據返回原接口
        return retryOldRequest.listen(response);
    } else {
        //節流:避免重複運行
       //去登錄
    }
};

// http response 響應攔截
axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) {
            //記錄最後操作時間
           localStorage.setItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
            return response;
        }
    },
    (err) => { 
        let res = err.response || {}; 
        if (res.data.meta?.statusCode == 302) {
            return refeshToken(res);
        } else {
            // 非302 報的錯誤; 
            return err;
        }
    }
);

複製代碼

以上便是我們這邊的業務,如果寫的不好請大佬多擔待~~

最後

如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流羣:1025263163相互學習,我們會有專業的技術答疑解惑

如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源項目點點star:http://github.crmeb.net/u/defu不勝感激 !