web-dev-qa-db-ja.com

Reduxを使用してJWTトークンを更新する方法

ReactネイティブReduxアプリはJWTトークンを認証に使用します。このようなトークンを必要とするアクションは多数あり、それらの多くはアプリのロード時などに同時にディスパッチされます。

例えば。

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

loadProfileloadAssetsの両方に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アクションの両方でトークンの更新が発生することでした。なぜなら、それらがディスパッチされるとトークンが期限切れになるためです。理想的には、トークンが更新されるまで認証を必要とするアクションを「一時停止」したいと思います。ミドルウェアでそれを行う方法はありますか?

42
lanan

これを解決する方法を見つけました。これがベストプラクティスのアプローチであるかどうかはわかりませんが、おそらくいくつかの改善が行われる可能性があります。

私の元々のアイデアは変わりません: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(dispatch).then(() => next(action));
                    } else {
                        return getState().auth.freshTokenPromise.then(() => next(action));
                    }
                }
            }
        }
        return next(action);
    };
}

最も重要な部分はrefreshToken関数です。その関数は、状態が新しいトークンのプロミスを含むように、トークンが更新されるときにアクションをディスパッチする必要があります。こうすると、トークン認証を同時に使用する複数の非同期アクションをディスパッチした場合、トークンは一度だけ更新されます。

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トークンの期限切れをreduxで処理する他の方法を教えてください。

35
lanan

アクションが完了するのを「待つ」代わりに、ストア変数を保持して、トークンをまだフェッチしているかどうかを知ることができます。

サンプルレデューサー

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());
       }
    };
}

これは、コンポーネントがマウントされたときに呼び出されます。認証キーが古い場合は、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()
    } 
}

マウントに物をロードしようとしますが、小道具を受け取ったときに特定の条件下でロードしようとすることに注意してください(ストアが変更されると呼び出され、fetchingを保持できます)。最初のフェッチが失敗すると、 refreshToken。それが完了すると、ストアに新しいトークンを設定し、コンポーネントを更新してcomponentWillReceivePropsを呼び出します。まだフェッチしていない場合(このチェックが必要かどうかわからない)、ロードします。

18
ZekeDroid

アクションを延期し、アクセストークンを更新するために、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();

トークンを状態に保ち、単純なヘルパーを使用してアクセストークンをリクエストヘッダーに挿入します

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更新トークンワークフローの動作を示しています

5
kmmbvnr