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

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

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

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

Execution queue

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

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

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

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

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

Ответы [ 3 ]

0 голосов
/ 22 февраля 2019

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

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 { 
      //...
    }
0 голосов
/ 27 февраля 2019

В вашем промежуточном программном обеспечении вы делаете 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/redux@4.0.1/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.log("dispatching 2", action);
    let result = next(action);
    console.log("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.log("dispatching", action);
  let result = next(action); // will return a pending Promise
  console.log("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.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({ // console.log of it's return value is too a pending `Promise`
  type: "INCREMENT"
});

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

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


Я не знаю всей вашей настройки, но я думаю, что-то подобное происходит в вашем случае.Вы предполагаете, что ваше промежуточное программное обеспечение что-то сделало и совершило круговое путешествие, но на самом деле оно еще не завершило работу (или никто не await велел ему закончить это).Возможно, вы отправляете действие для получения /templates, и поскольку вы написали промежуточное программное обеспечение для автоматического обновления токена носителя, вы предполагаете, что утилита извлечения вызова будет вызываться с совершенно новым токеном.Но 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. Запишите один Async Action Creator для всех вызовов API, который при необходимости обновит токен и асинхронно отправит действие только после обновления токена.

Пример с redux thunk :

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'])
0 голосов
/ 22 февраля 2019

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

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

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

...