如何使用Redux刷新JWT令牌?

时间:2016-04-29 22:40:54

标签: javascript reactjs react-native redux jwt

我们的React Native Redux应用程序使用JWT令牌进行身份验证。有许多动作需要这样的令牌,并且许多动作同时被派遣,例如当app加载时。

E.g。

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

}

我遇到的问题是,loadProfileloadAssets操作都会刷新令牌,因为在发送令牌时,令牌将会过期。理想情况下,我想“暂停”需要身份验证的操作,直到刷新令牌。有没有办法用中间件做到这一点?

4 个答案:

答案 0 :(得分:28)

我找到了解决这个问题的方法。我不确定这是否是最佳实践方法,并且可能会对其进行一些改进。

我最初的想法仍然存在: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中调度行动本身并不是行动。请告诉我您知道的任何其他方法,以处理使用redux到期的JWT令牌。

答案 1 :(得分:18)

而不是&#34;等待&#34;要完成某个操作,您可以保留一个商店变量,以便知道您是否还在提取令牌:

样品减速器

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

安装组件时会调用此方法。如果auth键是陈旧的,它将调度一个动作以将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() } } 设置为true并刷新令牌。请注意,我们尚未加载配置文件或资产。

新组件:

fetching

请注意,现在您尝试在挂载时加载您的东西,但在接收道具时也会在某些条件下加载(这将在商店更改时调用,以便我们可以保留refreshToken)当初始提取失败时,它将会触发componentWillReceiveProps。完成后,它会在商店中设置新令牌,更新组件,从而调用{{1}}。如果它仍未提取(不确定是否需要进行此检查),则会加载内容。

答案 2 :(得分:5)

我在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
      ]
  }
})

我写了article并分享了project example,它显示了JWT刷新令牌工作流程

答案 3 :(得分:0)

我认为 redux 不是强制执行令牌刷新原子性的正确工具。

相反,我可以为您提供一个可以从任何地方调用的原子函数,并确保您始终获得有效的令牌:

/*
    The non-atomic refresh function
*/

const refreshToken = async () => {
    // Do whatever you need to do here ...
}

/*
    Promise locking-queueing structure
*/

var promiesCallbacks = [];

const resolveQueue = value => {
  promiesCallbacks.forEach(x => x.resolve(value));
  promiesCallbacks = [];
};
const rejectQueue = value => {
  promiesCallbacks.forEach(x => x.reject(value));
  promiesCallbacks = [];
};
const enqueuePromise = () => {
  return new Promise((resolve, reject) => {
    promiesCallbacks.push({resolve, reject});
  });
};

/*
    The atomic function!
*/

var actionInProgress = false;

const refreshTokenAtomically = () => {
  if (actionInProgress) {
    return enqueuePromise();
  }

  actionInProgress = true;

  return refreshToken()
    .then(({ access }) => {
      resolveQueue(access);
      return access;
    })
    .catch((error) => {
      rejectQueue(error);
      throw error;
    })
    .finally(() => {
      actionInProgress = false;
    });
};

也在此处发布:https://stackoverflow.com/a/68154638/683763