Как отловить момент "протухания" access token jwt?

Делаю фронт и столкнулся с вопросом: бэк выписал мне access & refresh токены, и как отловить момент когда access токен прокис и отправить запрос на выдачу нового?
По большей части хотелось бы теоретического совета, но если будут и практические примеры, то не откажусь)
Фронт я пишу на вью, запросы шлю на бэк через axios


Ответы (2 шт):

Автор решения: Dmitry

Теория.

Вместе с токенами на доступ и обновление приходит и время действия токена. Обычно под ключом exp или expire. Это время истечения токена доступа.

Для того, чтобы отловить момент замены, нужно просто сравнить время сейчас и время указанное под ключом exp. При этом, чтобы не ловить секунды можно установить некое отклонение. Напимер, если разница времени сейчас и указанном в exp меньше 2 минут, запросить новый токен

→ Ссылка
Автор решения: Total Pusher

Axios имеет перехватчики Interceptors, через которые можно отловить момент отправки запроса.

В этот момент можно добавить заголовки авторизации, таким образом это делается только в одном месте, а не раскидывается по всему коду, где используется axios (DRY). А также можно проверить токен на предмет протухания и запросить новый.

Я когда-то взял за основу этот сниппет, и сделал свою реализацию в виде модуля.

Модуль для работы с Axios axios/index.js:

import axios from "axios";
import jwt_decode from "jwt-decode"; // необходимо установить пакет jwt-decode

/**
 * Префикс API-роутов
 */
const axiosInstance = axios.create({
    baseURL: "/api/"
});

/**
 * Роуты, которые не требуют авторизацию.
 */
function noAuthRoute(uri) {
    return /^(auth\/|password\/|register$)/.test(uri);
}

/**
 * Начало отправки запроса:
 * проверка просрочки токена, выдача нового перед отправкой запроса
 */
axiosInstance.interceptors.request.use(async function (config) {
    return new Promise(async resolve => {
        // все запросы, где не нужна авторизация, пропускаем без доп. обработки
        // например, запрос на авторизацию
        if (noAuthRoute(config.url)) {
            resolve(config);
            return;
        }

        try {
            /**
             * Тут используется Vuex, через который получаем объект пользователя user,
             * в котором содержится access_token.
             * Поправьте реализацию под свой код.
             */
            let user = window.app.$store.state.auth.user;
            let payload = jwt_decode(user.access_token);

            /**
             * Получаем текущее время и время устаревания токена
             */
            let expTime = payload.exp * 1000;
            let curTime = new Date().getTime();

            /**
             * Сравниваем, если разница более 3 секунд - запускаем процедуру получения нового токена из refresh_token
             * Доработайте эту часть под свой код.
             */
            if (expTime - curTime <= -3000) {
                await window.app.$store.dispatch("auth/refreshToken");
            }

            /**
             * Авто-добавление заголовков авторизации.
             * Тут также используется Vuex, доработайте под свой код.
             */
            config.headers = {...config.headers, ...window.app.$store.getters["auth/authHeader"]};

            resolve(config);
        } catch (err) {
            console.log("axiosInstance.interceptors.request.use", err);
            resolve(config);
        }
    });
}, function (error) {
    // Do something with request error
    return Promise.reject(error);
});

/**
 * Получение ответа:
 * если запрос отправлен, но сервер все равно вернул 401 - показываем модальное окно повторного входа
 */
axiosInstance.interceptors.response.use(function (response) {
    /**
     * Любой код состояния, находящийся в диапазоне 2xx, вызывает срабатывание этой функции.
     * Тут можно что-то сделать.
     */
    return response;
}, function (error) {
    /**
     * Любые коды состояния, выходящие за пределы диапазона 2xx, вызывают срабатывание этой функции.
     * Тут можно что-то сделать.
     */

    try {
        if (noAuthRoute(error.response.config.url)) {
            return Promise.reject(error);
        }

        /**
         * Тут вызывается модальное окно для повторно ввода логина-пароля.
         */
        if (error.response.status === 401) {
            window.app.$refs.refreshTokenModal.show({
            });
        }
    } catch (e) {

    }

    return Promise.reject(error);
});

export default axiosInstance;

Как работать с этим модулем? Можно напрямую:

import axios from "../axios";
// ...
axios.get(...
axios.post(...

Однако лучше сделать отдельные сервисы, и работать уже с ними.

Например, список компаний можно получить так:

import CompanyService from "../services/CompanyService";
// ...
let resp = await CompanyService.getTransporterCompanies();

Лучше читается чем куча манипуляций с axios? Думаю, да. А вот и сам сервис, в который спрятали работу с беком CompanyService.js:

import axios from "../axios";

class CompanyService {

    // Список компаний  
    getTransporterCompanies() {
        return axios.get("company?perPage=1000&filter[contractorType.slug]=transporter&fields[company]=id,name");
    }

    // Блокировка компании
    block(id, data) {
        return axios.post(`company/${id}/block`, data);
    }
}

export default new CompanyService();
→ Ссылка