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

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

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.info({"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.info(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.info({"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 вызывается до (а не после) моей конечной точки /token/refresh с использованием старого токена с истекшим сроком действия...
  • Следствием этого является то, что мой текущий экран возвращает ошибку (Unauthorized), но если пользователь изменит экран, он снова будет работать, поскольку его токен был успешно обновлен. Но это некрасиво так :p

Обновлено: Ради этой проблемы я переработал свой код, чтобы поместить его в один файл. Я также поместил файл console.info, чтобы показать, как этот код будет выполняться.

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

Мы можем видеть из изображения, что:

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

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

РЕДАКТИРОВАТЬ до конца награды:

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

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
16
0
2 799
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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

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

Привет, я уже рассмотрел большинство твоих вариантов. На самом деле я ищу решение, используя мой контекст (промежуточное ПО) или действительно близкое решение.

Greco Jonathan 22.02.2019 18:17
Ответ принят как подходящий

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

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 { 
      //...
    }

Привет, спасибо за ваше предложение, оно, конечно, может сработать, можете ли вы объяснить, почему мой метод не работает? Ваше решение включает в себя множество условий в моих редукторах, которые я не хочу делать, если у меня есть другой вариант. Промежуточное ПО должно работать правильно?

Greco Jonathan 22.02.2019 18:19

За ваш прагматизм и решение у вас будет правильный ответ. Я проверил это, и это сработало, моя проблема решена с помощью вашего способа.

Greco Jonathan 28.02.2019 16:20

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

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

const logger = store => next => action => {
  console.info('dispatching', action)
  let result = next(action)
  console.info('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.info('dispatching', action)
  let result = next(action) // and you return whatever the current version is supposed to return
  console.info('next state', store.getState())
  return result
}

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

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.info("dispatching", action);
  let result = next(action);
  console.info("next state", store.getState());
  return result;
};

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

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

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

const store = createStore(rootReducer, middlewareEnhancer);

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

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

Сначала logger получает действие, затем logger2, затем logger3, а затем переходит к фактическому store.dispatch и вызывается редюсер. Редюсер изменяет состояние с 0 на 1, а logger3 получает обновленное состояние и передает возвращаемое значение (действие) обратно в logger2, а затем в logger.

Теперь давайте рассмотрим, что происходит, когда вы меняете store.dispatch на асинхронную функцию где-то в середине цепочки:

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(5000).then(v => {
    console.info("dispatching 2", action);
    let result = next(action);
    console.info("next state 2", store.getState());
    return result;
  });
};

Я изменил logger2, но logger (тот, что выше по цепочке) понятия не имеет, что next теперь асинхронно. Он вернет ожидание Promise и вернется с «необновленным» состоянием, потому что отправленное действие еще не достигло редьюсера.

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.info("dispatching", action);
  let result = next(action); // will return a pending Promise
  console.info("next state", store.getState());
  return result;
};

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(2000).then(() => {
    console.info("dispatching 2", action);
    let result = next(action);
    console.info("next state 2", store.getState());
    return result;
  });
};

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

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

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({ // console.info of it's return value is too a pending `Promise`
  type: "INCREMENT"
});

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

Таким образом, мой store.dispatch немедленно возвращается из цепочки промежуточного программного обеспечения с этим ожидающим промисом, а console.info('current state', store.getState()); по-прежнему печатает 0. Действие достигает исходного store.dispatch, а редюсер еще долго после этого.


Я не знаю всей вашей установки, но я предполагаю, что что-то подобное происходит в вашем случае. Вы предполагаете, что ваше промежуточное ПО что-то сделало и совершило обход, но на самом деле оно не закончило работу (или никто await не заставил его закончить ее). Возможно, вы отправляете действие для извлечения /templates, и, поскольку вы написали промежуточное ПО для автоматического обновления токена носителя, вы предполагаете, что вспомогательная утилита fetch будет вызываться с совершенно новым токеном. Но dispatch вернулся рано с ожидающим обещанием, и ваш токен все еще старый.

Кроме того, только одна вещь кажется явно неправильной: вы дважды отправляете одно и то же действие в промежуточном программном обеспечении через next:

const tokenMiddleware = store => next => async action => {
  if (something) {
    if (something) {
      await fetch('/token/refresh',)
        .then(async (data) => {
            return await Promise.all([
              // ...
            ]).then((values) => {
              return next(action); // First, after the `Promise.all` resolves
            });
        });
      return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
    } else {
      return next(action);
    }
  }

Рекомендации:

  1. Храните свои токены в хранилище избыточности, сохраняйте их в хранилище и повторно гидратируйте хранилище избыточности из хранилища.
  2. Напишите один Создатель асинхронных действий для всех вызовов API, который при необходимости будет обновлять токен и отправлять действие асинхронно только после обновления токена.

Пример с избыточный преобразователь:

function apiCallMaker(dispatch, url, actions) {
  dispatch({
    type: actions[0]
  })

  return fetch(url)
    .then(
      response => response.json(),
      error => {
        dispatch({
          type: actions[2],
          payload: error
        })
      }
    )
    .then(json =>
      dispatch({
        type: actions[1],
        payload: json
      })
    )
  }
}

export function createApiCallingActions(url, actions) {
  return function(dispatch, getState) {

    const { accessToken, refreshToken } = getState();
    if (neededToRefresh) {
      return fetch(url)
        .then(
          response => response.json(),
          error => {
            dispatch({
              type: 'TOKEN_REFRESH_FAILURE',
              payload: error
            })
          }
        )
        .then(json =>
          dispatch({
              type: 'TOKEN_REFRESH_SUCCESS',
              payload: json
          })
          apiCallMaker(dispatch, url, actions)
        )
    } else {
      return apiCallMaker(dispatch, url, actions)
    }
}

Вы бы использовали его так:

dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])

dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])

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

Greco Jonathan 27.02.2019 21:32

Я использую redux-persist для хранения некоторых моих редукторов. Ваша рекомендация номер один сложна :p. У вашего apiCallMaker есть ошибка, верно? при ошибке вы не можете вызвать action[2] not [1] ?

Greco Jonathan 28.02.2019 08:21

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

Greco Jonathan 28.02.2019 16:22

Другие вопросы по теме