【问题标题】:How to use Redux to refresh JWT token?如何使用 Redux 刷新 JWT 令牌?
【发布时间】:2016-08-25 05:14:09
【问题描述】:

我们的 React Native 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);
};

}

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

【问题讨论】:

  • 我建议你看一个名为redux-saga的库...它完美地解决了这个问题。
  • @KevinHe:能多分享一下 redux-saga 是如何解决这个问题的吗?

标签: javascript reactjs react-native redux jwt


【解决方案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(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 令牌的方法。

【讨论】:

  • 你可以让 refreshToken 接收一个“postponedAction”,如果刷新成功完成而不是返回一个新的 Promise,它将被调度。至少我是这样解决的。
  • @Shvetusya 我不会担心在 refreshToken 中调度动作本身不是动作。 refreshToken 本质上是一个动作创建者,像这样在 actionCreator 中调度其他动作是很常见的做法
  • 感谢这段代码!也许在所有操作之后,我们需要从状态中删除 freshTokenPromise 对象? return getState() .auth.freshTokenPromise.then(() => next(action)) .then(() => { dispatch({ type: REFRESHING_TOKEN_PROMISE_CLEAN, freshTokenPromise: null, }) })
  • 漂亮!对于redux-persist 的人的一点说明,不支持坚持承诺,freshTokenPromise 必须使用转换器排除/列入黑名单
  • @Jawla 这是一个例子gist.github.com/hatemalimam/5e196f4953f50187b130600f62a99856希望对您有所帮助
【解决方案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 上加载您的东西,但在接收道具时也在某些条件下(这将在 store 更改时被调用,因此我们可以将 fetching 保留在那里)当初始获取失败时,它将触发refreshToken。完成后,它将在存储中设置新令牌,更新组件并因此调用componentWillReceiveProps。如果它仍未获取(不确定是否需要此检查),它将加载内容。

【讨论】:

  • 谢谢!这对于初始负载绝对有意义。但我不确定它是否适用于应用程序加载并使用后过期的令牌。对 API 的每次调用都需要有效的令牌。我们有许多需要登录和加载数据的弹出视图,所以我不确定通过这些视图的道具处理过期是否有效。
  • 您可以更改逻辑以检查令牌过期而不是令牌差异。这个想法是任何动作都会触发这个生命周期方法,所以你可以利用它来更新fetching变量并做出相应的反应
  • 我在需要 JWT 的每个操作中添加 dispatch({ type: 'LOAD_FETCHING', fetching: true }) 的第一个问题是代码重复。第二个问题是如何知道刷新何时完成。假设有一个“添加到收藏夹”按钮,它调度需要身份验证的 api 调用。我是否要在该操作中添加“如果令牌过期刷新然后拨打电话”逻辑?其他类似的动作呢?这就是我尝试使用中间件的原因。在其他框架/语言中,我使用过装饰器,但我不确定是否可以使用 React 来做到这一点。
  • 啊,是的,它会重复,绝对应该是中间件。装饰器是有意义的,但我不确定你也可以使用它们。另一种策略是通过中间件将您的操作(例如'ADD_TO_FAVS')“排队”到队列数组中。立即尝试分发,但如果令牌已过时,请刷新它。同时,订阅此更改并在任何更改尝试清空队列时进行。调度会有延迟,但不会超过这种握手的预期。
【解决方案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 nextCheckPostponed = (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(nextCheckPostponed)(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 刷新令牌工作流程的实际应用

【讨论】:

    【解决方案4】:

    我认为 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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 2022-01-19
      • 2016-03-05
      • 2016-06-25
      • 2022-12-23
      相关资源
      最近更新 更多