Реакция собственного обновления работает, но при следующем вызове по-прежнему используется последний токен

Я использую следующее промежуточное ПО для обновления токена по истечении срока его действия:

import {AsyncStorage} from 'react-native';
import moment from 'moment';
import fetch from "../components/Fetch";
import jwt_decode from 'jwt-decode';

/**
 * This middleware is meant to be the refresher of the authentication token, on each request to the API,
 * it will first call refresh token endpoint
 * @returns {function(*=): Function}
 * @param store
 */
const tokenMiddleware = store => next => async action => {
  if (typeof action === 'object' && action.type !== "FETCHING_TEMPLATES_FAILED") {
    let eToken = await AsyncStorage.getItem('eToken');
    if (isExpired(eToken)) {
      let rToken = await AsyncStorage.getItem('rToken');

      let formData = new FormData();
      formData.append("refresh_token", rToken);

      await fetch('/token/refresh',
        {
          method: 'POST',
          body: formData
        })
        .then(response => response.json())
        .then(async (data) => {
            let decoded = jwt_decode(data.token);
            console.log({"refreshed": data.token});

            return await Promise.all([
              await AsyncStorage.setItem('token', data.token).then(() => {return AsyncStorage.getItem('token')}),
              await AsyncStorage.setItem('rToken', data.refresh_token).then(() => {return AsyncStorage.getItem('rToken')}),
              await AsyncStorage.setItem('eToken', decoded.exp.toString()).then(() => {return AsyncStorage.getItem('eToken')}),
            ]).then((values) => {
              return next(action);
            });
        }).catch((err) => {
          console.log(err);
        });

      return next(action);
    } else {
      return next(action);
    }
  }

  function isExpired(expiresIn) {
    // We refresh the token 3.5 hours before it expires(12600 seconds) (lifetime on server  25200seconds)
    return moment.unix(expiresIn).diff(moment(), 'seconds') < 10;
  }
};
  export default tokenMiddleware;

И помощник по поиску

import { AsyncStorage } from 'react-native';
import GLOBALS from '../constants/Globals';
import {toast} from "./Toast";
import I18n from "../i18n/i18n";

const jsonLdMimeType = 'application/ld+json';

export default async function (url, options = {}, noApi = false) {
  if ('undefined' === typeof options.headers) options.headers = new Headers();
  if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);

  if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
    options.headers.set('Content-Type', jsonLdMimeType);
  }

  let token = await AsyncStorage.getItem('token');
  console.log({"url": url,"new fetch": token});
  if (token) {
    options.headers.set('Authorization', 'Bearer ' + token);
  }

  let api = '/api';

  if (noApi) {
    api = "";
  }

  const link = GLOBALS.BASE_URL + api + url;
  return fetch(link, options).then(response => {
    if (response.ok) return response;

    return response
      .json()
      .then(json => {
        if (json.code === 401) {
          toast(I18n.t(json.message), "danger", 3000);
          AsyncStorage.setItem('token', '');
        }

        const error = json['message'] ? json['message'] : response.statusText;
        throw Error(I18n.t(error));
      })
      .catch(err => {
        throw err;
      });
  })
  .catch(err => {
    throw err;
  });
}

Моя проблема:

  • когда я совершаю действие, промежуточное ПО называется.
  • Если срок действия токена истекает, вызывается метод обновления токена и обновляется AsyncStorage.
  • Затем должен вызываться метод next(action).
  • Но моя конечная точка /templates вызывается до (а не после) конечной точки my /token/refresh с использованием старого просроченного токена...
  • Тогда следствием этого является то, что мой текущий экран возвращает ошибку (неавторизовано), но если пользователь изменит экран, он снова будет работать, поскольку его токен был успешно обновлен. Но это так безобразно: p

РЕДАКТИРОВАТЬ: Ради этой проблемы, я переработал свой код, чтобы поместить это в один файл. Я также поместил немного console.log, чтобы показать, как этот код будет выполняться

Execution queue

Из изображения видно, что:

  • Мои вызовы (/шаблоны) выполняются до моей конечной точки обновления. И мой консольный журнал обновленного токена приходит спустя много времени после этого...

Любая помощь в этом, пожалуйста?

РЕДАКТИРОВАТЬ до конца щедрости:

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

Ответы

Ответ 1

У меня немного другая настройка в обработке. Вместо того, чтобы обрабатывать логику обновления токенов в промежуточном программном обеспечении, я определяю ее как вспомогательную функцию. Таким образом, я могу выполнить всю проверку токенов прямо перед любым сетевым запросом, если я считаю нужным, и любое действие при избыточности, которое не включает сетевой запрос, не будет нуждаться в этой функции

export const refreshToken = async () => {
  let valid = true;

  if (!validateAccessToken()) {
    try {
      //logic to refresh token
      valid = true;
    } catch (err) {
      valid = false;
    }

    return valid;
  }
  return valid;
};

const validateAccessToken = () => {
  const currentTime = new Date();

  if (
    moment(currentTime).add(10, 'm') <
    moment(jwtDecode(token).exp * 1000)
  ) {
    return true;
  }
  return false;
};

Теперь, когда у нас есть эта вспомогательная функция, я вызываю ее для всех необходимых действий при необходимости

const shouldRefreshToken = await refreshToken();
    if (!shouldRefreshToken) {
      dispatch({
        type: OPERATION_FAILED,
        payload: apiErrorGenerator({ err: { response: { status: 401 } } })
      });
    } else { 
      //...
    }

Ответ 2

В вашем промежуточном программном обеспечении вы делаете store.dispatch асинхронным, но оригинальная подпись store.dispatch является синхронной. Это может иметь серьезные побочные эффекты.

Давайте рассмотрим простое промежуточное ПО, которое регистрирует каждое действие, которое происходит в приложении, вместе с состоянием, вычисленным после него:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

Написание вышеупомянутого промежуточного программного обеспечения по существу делает следующее:

const next = store.dispatch  // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) {  // you change it to meet your needs
  console.log('dispatching', action)
  let result = next(action) // and you return whatever the current version is supposed to return
  console.log('next state', store.getState())
  return result
}

Рассмотрим этот пример с 3 такими промежуточными программами, соединенными вместе:

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => action => {
  console.log("dispatching 2", action);
  let result = next(action);
  console.log("next state 2", store.getState());
  return result;
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

Ответ 3

У вас есть условие гонки запросов, и нет правильного решения, которое полностью решило бы эту проблему. Следующие элементы могут быть использованы в качестве отправной точки для решения этой проблемы:

  • Используйте обновление токена отдельно и дождитесь его выполнения на стороне клиента, например, отправьте обновление токена (что-то вроде GET/keepalive) в случае, если какой-либо запрос был отправлен за половину периода времени ожидания сеанса - это приведет к тому, что все запросы будут 100% авторизация (опция, которую я бы определенно использовал - она также может использоваться для отслеживания не только запросов, но и событий)
  • Очистить токен после получения 401 - вы не увидите работающее приложение после перезагрузки, если предположить, что удаление действительного токена в случае граничных сценариев является положительным сценарием (Простое в реализации решение)
  • Повторите запрос, который получил 401 с некоторой задержкой (на самом деле не лучший вариант)
  • Принудительно обновляйте токены чаще, чем время ожидания - изменение их на 50-75% времени ожидания уменьшит количество неудачных запросов (но они все равно будут сохраняться, если пользователь простаивал в течение всего времени сеанса). Таким образом, любой действительный запрос вернет новый действующий токен, который будет использоваться вместо старого.

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