Я использую следующее промежуточное ПО для обновления своего токена по истечении срока его действия:
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;
});
}
Моя проблема:
next(action)./templates вызывается до (а не после) моей конечной точки /token/refresh с использованием старого токена с истекшим сроком действия...Обновлено: Ради этой проблемы я переработал свой код, чтобы поместить его в один файл. Я также поместил файл console.info, чтобы показать, как этот код будет выполняться.
Мы можем видеть из изображения, что:
Любая помощь в этом, пожалуйста?
РЕДАКТИРОВАТЬ до конца награды:
Из этого вопроса я пытаюсь понять, почему мой подход к промежуточному программному обеспечению неверен, поскольку многие ресурсы, которые я нашел в Интернете, говорят о промежуточном программном обеспечении как о лучшем решении для выполнения операций с токенами обновления.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


У вас есть состояние гонки запросов, и нет правильного решения, которое полностью решит эту проблему. Следующие пункты могут быть использованы в качестве отправной точки для решения этой проблемы:
Принудительное обновление токенов чаще, чем время ожидания — изменение их на 50-75% времени ожидания уменьшит количество неудачных запросов (но они все равно будут сохраняться, если пользователь бездействовал в течение всего времени сеанса). Таким образом, любой действительный запрос вернет новый действительный токен, который будет использоваться вместо старого.
Реализовать период продления токена, когда старый токен может считаться действительным в течение периода передачи - старый токен продлевается на некоторое ограниченное время, чтобы обойти проблему (звучит не очень хорошо, но это, по крайней мере, вариант)
У меня немного другая установка в обращении. Вместо обработки логики токена обновления в промежуточном программном обеспечении я определяю ее как вспомогательную функцию. Таким образом, я могу выполнять всю проверку токена прямо перед любым сетевым запросом, где я считаю нужным, и любое избыточное действие, не связанное с сетевым запросом, не будет нуждаться в этой функции.
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 {
//...
}
Привет, спасибо за ваше предложение, оно, конечно, может сработать, можете ли вы объяснить, почему мой метод не работает? Ваше решение включает в себя множество условий в моих редукторах, которые я не хочу делать, если у меня есть другой вариант. Промежуточное ПО должно работать правильно?
За ваш прагматизм и решение у вас будет правильный ответ. Я проверил это, и это сработало, моя проблема решена с помощью вашего способа.
В вашем промежуточном программном обеспечении вы делаете 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);
}
}
Рекомендации:
Пример с избыточный преобразователь:
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'])
эй, позвольте мне сказать вам, что я рад вашему ответу, обязательно попробую это завтра. Большое спасибо за время, чтобы написать это.
Я использую redux-persist для хранения некоторых моих редукторов. Ваша рекомендация номер один сложна :p. У вашего apiCallMaker есть ошибка, верно? при ошибке вы не можете вызвать action[2] not [1] ?
За ваше объяснение, вы заработали награду. Действительно, это была моя проблема. Но для ее решения требуется слишком много рефакторинга, я предпочел реализовать другое решение (самое быстрое, время - деньги). Спасибо за вашу работу над этим.
Привет, я уже рассмотрел большинство твоих вариантов. На самом деле я ищу решение, используя мой контекст (промежуточное ПО) или действительно близкое решение.