Подтвердить что ты не робот

Как использовать Redux для обновления токена JWT?

Наше приложение React Native Redux использует токены JWT для аутентификации. Существует много действий, требующих таких токенов, и многие из них отправляются одновременно, например. когда приложение загружается.

например.

componentDidMount() {
    dispath(loadProfile());
    dispatch(loadAssets());
    ...
}

Оба loadProfile и loadAssets требуют JWT. Мы сохраняем токен в состоянии и AsyncStorage. Мой вопрос заключается в том, как справиться с истечением срока действия токена.

Изначально я собирался использовать промежуточное программное обеспечение для обработки срока действия токена

// jwt-middleware.js

export function refreshJWTToken({ dispatch, getState }) {

  return (next) => (action) => {
    if (isExpired(getState().auth.token)) {
      return dispatch(refreshToken())
          .then(() => next(action))
          .catch(e => console.log('error refreshing token', e));
    }
    return next(action);
};

}

Проблема, с которой я столкнулась, заключалась в том, что обновление токена произойдет как для действий loadProfile, так и loadAssets, потому что в момент их отправки токен будет истек. В идеале я хотел бы "приостановить" действия, требующие аутентификации, до тех пор, пока токен не будет обновлен. Есть ли способ сделать это с помощью промежуточного программного обеспечения?

4b9b3361

Ответ 1

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

Моя первоначальная идея остается: обновление JWT находится в промежуточном программном обеспечении. Это промежуточное программное обеспечение должно пройти до thunk, если используется thunk.

...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);

Затем в коде промежуточного программного обеспечения мы проверяем, не истечет ли токен перед любым асинхронным действием. Если это истекло, мы также проверяем, действительно ли мы обновляем токен - чтобы иметь такую ​​проверку, мы добавляем обещание для нового токена в состояние.

import { refreshToken } from '../actions/auth';

export function jwt({ dispatch, getState }) {

    return (next) => (action) => {

        // only worry about expiring token for async actions
        if (typeof action === 'function') {

            if (getState().auth && getState().auth.token) {

                // decode jwt so that we know if and when it expires
                var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;

                if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {

                    // make sure we are not already refreshing the token
                    if (!getState().auth.freshTokenPromise) {
                        return refreshToken().then(() => next(action));
                    } else {
                        return getState().auth.freshTokenPromise.then(() => next(action));
                    }
                }
            }
        }
        return next(action);
    };
}

Наиболее важной частью является функция refreshToken. Эта функция должна отправлять действие, когда токен обновляется, так что состояние будет содержать обещание для нового токена. Таким образом, если мы отправляем несколько асинхронных действий, которые используют токен auth одновременно, токен обновляется только один раз.

export function refreshToken(dispatch) {

    var freshTokenPromise = fetchJWTToken()
        .then(t => {
            dispatch({
                type: DONE_REFRESHING_TOKEN
            });

            dispatch(saveAppToken(t.token));

            return t.token ? Promise.resolve(t.token) : Promise.reject({
                message: 'could not refresh token'
            });
        })
        .catch(e => {

            console.log('error refreshing token', e);

            dispatch({
                type: DONE_REFRESHING_TOKEN
            });
            return Promise.reject(e);
        });



    dispatch({
        type: REFRESHING_TOKEN,

        // we want to keep track of token promise in the state so that we don't try to refresh
        // the token again while refreshing is in process
        freshTokenPromise
    });

    return freshTokenPromise;
}

Я понимаю, что это довольно сложно. Я также немного беспокоюсь о том, чтобы отправлять действия в refreshToken, который не является самим действием. Пожалуйста, дайте мне знать о любом другом подходе, который вы знаете, который обрабатывает истечение токена JWT с помощью сокращения.

Ответ 2

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

Редуктор для образцов

const initialState = {
    fetching: false,
};
export function reducer(state = initialState, action) {
    switch(action.type) {
        case 'LOAD_FETCHING':
            return {
                ...state,
                fetching: action.fetching,
            }
    }
}

Теперь создатель действия:

export function loadThings() {
    return (dispatch, getState) => {
        const { auth, isLoading } = getState();

        if (!isExpired(auth.token)) {
            dispatch({ type: 'LOAD_FETCHING', fetching: false })
            dispatch(loadProfile());
            dispatch(loadAssets());
       } else {
            dispatch({ type: 'LOAD_FETCHING', fetching: true })
            dispatch(refreshToken());
       }
    };
}

Это вызвано, когда компонент установлен. Если ключ auth устарел, он отправит действие, чтобы установить fetching в true, а также обновить токен. Обратите внимание, что мы еще не собираемся загружать профиль или активы.

Новый компонент:

componentDidMount() {
    dispath(loadThings());
    // ...
}

componentWillReceiveProps(newProps) {
    const { fetching, token } = newProps; // bound from store

    // assuming you have the current token stored somewhere
    if (token === storedToken) {
        return; // exit early
    }

    if (!fetching) {
        loadThings()
    } 
}

Обратите внимание, что теперь вы пытаетесь загрузить свои вещи на mount, но также при определенных условиях при получении реквизита (это вызывается, когда хранилище изменяется, поэтому мы можем сохранить fetching там). Когда исходная выборка завершится с ошибкой, она вызовет refreshToken. Когда это будет сделано, он установит новый токен в хранилище, обновит компонент и, следовательно, вызовет componentWillReceiveProps. Если он еще не извлечен (не уверен, что эта проверка необходима), он будет загружать вещи.

Ответ 3

Я сделал простую оболочку вокруг redux-api-middleware, чтобы отложить действия и обновить токен доступа.

middleware.js

import { isRSAA, apiMiddleware } from 'redux-api-middleware';

import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'


export function createApiMiddleware() {
  const postponedRSAAs = []

  return ({ dispatch, getState }) => {
    const rsaaMiddleware = apiMiddleware({dispatch, getState})

    return (next) => (action) => {
      const nextCheckPostoned = (nextAction) => {
          // Run postponed actions after token refresh
          if (nextAction.type === TOKEN_RECEIVED) {
            next(nextAction);
            postponedRSAAs.forEach((postponed) => {
              rsaaMiddleware(next)(postponed)
            })
          } else {
            next(nextAction)
          }
      }

      if(isRSAA(action)) {
        const state = getState(),
              token = refreshToken(state)

        if(token && isAccessTokenExpired(state)) {
          postponedRSAAs.push(action)
          if(postponedRSAAs.length === 1) {
            return  rsaaMiddleware(nextCheckPostoned)(refreshAccessToken(token))
          } else {
            return
          }
        }

        return rsaaMiddleware(next)(action);
      }
      return next(action);
    }
  }
}

export default createApiMiddleware();

Я храню токены в состоянии и использую простой помощник для ввода токена Acess в заголовки запроса

export function withAuth(headers={}) {
  return (state) => ({
    ...headers,
    'Authorization': `Bearer ${accessToken(state)}`
  })
}

Значения redux-api-middleware остаются практически неизменными

export const echo = (message) => ({
  [RSAA]: {
      endpoint: '/api/echo/',
      method: 'POST',
      body: JSON.stringify({message: message}),
      headers: withAuth({ 'Content-Type': 'application/json' }),
      types: [
        ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
      ]
  }
})

Я написал статью и поделился примером , который показывает рабочий поток токенов JWT в действии