После истечения сеанса токен обновляется и интерфейс повторно запрашивается (режим публикации подписки).

внешний интерфейс
После истечения сеанса токен обновляется и интерфейс повторно запрашивается (режим публикации подписки).

предисловие


❝ Недавно наш начальник попросил Сяобая поработать над модулем входа в систему, который бывает простым и простым, сложным и сложным. В этой главе в основном рассказывается о ряде вещей, которые происходят, когда токен обновляется после истечения сеанса. ❞

нужно

На странице, когда запрос терпит неудачу и возвращает 302, определяется, истек ли срок действия интерфейса или истек срок действия входа.Если срок действия интерфейса истек, он запросит новый токен, а затем использует новый токен, чтобы снова инициировать запрос. .

интерфейс 1
запрос не выполнен 302
Срок действия входа истек
перерегистрировать
интерфейс 2
сеанс истек
обновить токен
новый токен
Повторно запросить указанный выше неисправный интерфейс
интерфейс 3

идеи


  • В начале я думал о черной технологии (за лень), то есть после получения нового токена я принудительно обновляю страницу, чтобы интерфейс в странице автоматически обновлялся~(удобство удобное, но пользовательский опыт не очень хороший)
  • В настоящее время, когда вы думаете о повторном запросе интерфейса, вы можете сотрудничать с режимом публикации подписки, чтобы улучшить взаимодействие с пользователем.

Перехват ответа

Сначала делаем запросaxios({url:'/test',data:xxx}).then(res=>{})

После перехвата 302 входим в логику обновления токена

Код перехвата ответа

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;
        }
    }
);

Формат данных в нашем фоне основан на коде состояния, чтобы судить об истечении срока действия (вы можете судить в соответствии с вашей реальной ситуацией), а затем введитеrefrshTokenметод~

метод обновления токена

//避免其他接口同时请求(只请求一次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Что это? Правильно, это наша мужская очередь с двумя режимами подписки и публикации.

ПОДПИСАТЬСЯ РЕЖИМ ПУБЛИКАЦИИ


Если вы не знаетеПОДПИСАТЬСЯ РЕЖИМ ПУБЛИКАЦИИ, вы можете щелкнуть, чтобы увидеть это, есть простые для понимания примеры, написанные великим богом (если вы думаете, что выучили это, кстати, вам это может понравиться~).

Используйте отказавший интерфейс в качестве подписчика и успешно получите новый токен перед публикацией (запросите интерфейс).

Ниже приведен код режима подписки-публикации.

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 = [];
    },
};

Сначала вам не нужно обращать внимание на логику подписчика, вам нужно только знать, что абонент - это интерфейс (ответ) после завершения неудачных запросов.

каждый раз, когда вы входитеrefeshTokenметод, наш неисправный интерфейс вызоветretryOldRequest.listenподписаться, и нашиrequestQueryИменно очередь удерживает этих подписчиков.

注意: наша очередь подписчиковrequestQueryэто способ сохранить для публикации. И после успешного получения нового токена,retryOldRequest.triggerОн будет публиковать эти сообщения (новый токен) для подписчиков (метод, запускающий очередь подписки).

пока подписчик(response) содержит конфигурацию конфига. После получения нового токена (после публикации) модифицируем в конфиге заголовок запроса Autorzation. С помощью Promise мы можем лучше получить данные интерфейса, запрашиваемые новым токеном. просили, мы можем вернуть исходный интерфейс без изменений/test(потому что то, что мы возвращаем в перехвате ответа,refreshToken,а такжеrefreshTokenВозвращает подписчикаretryOldRequest.listenВозвращаемые данные, а прослушиватель возвращает данные промиса, а промис разрешается после успешного запроса).

Увидев это, вы чувствуете себя немного сбитым с толку~

В реальной разработке наша логика также включает истечение срока действия входа (в отличие от срока действия запроса). мы основаны на 当前时间 - 过去时间 < expiresTime(epiresTime: допустимое время, возвращенное после входа в систему), чтобы определить, истек ли срок действия запроса или срок действия входа. Ниже приведена полная логика

после входа
получить expiresTime
интерфейс 1
запрос не выполнен 302
Судя по expiresTime
Срок действия входа истек
перерегистрировать
интерфейс 2
сеанс истек
обновить токен
новый токен
Повторно запросить указанный выше неисправный интерфейс
интерфейс 3

Ниже приведен полный код

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;
        }
    }
);

Вышеупомянутое является нашим бизнесом.Если письмо плохое, пожалуйста, потерпите меня~~

Если у вас есть хороший план, вы также можете обсудить его друг с другом в комментариях~