會話過期後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不勝感激 !