首页 文章

如何使用Redux刷新JWT令牌?

提问于
浏览
36

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

例如 .

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 动作都会刷新令牌,因为在它们发送时令牌将过期 . 理想情况下,我想要在刷新令牌之前需要身份验证的"pause"操作 . 有没有办法用中间件做到这一点?

3 回答

  • 18

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

    我最初的想法仍然存在: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 功能 . 该函数需要在刷新令牌时调度操作,以便状态将包含新令牌的承诺 . 这样,如果我们同时发送多个使用令牌身份验证的异步操作,则令牌只会刷新一次 .

    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令牌 .

  • 5

    您可以改为使用商店变量来知道您是否仍在获取令牌,而不是“等待”某个操作完成:

    样品减速机

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

    请注意,现在您尝试在挂载时加载您的东西,但在接收道具时也会在某些条件下加载(这将在商店更改时调用,因此我们可以保留 fetching )当初始提取失败时,它将触发 refreshToken . 完成后,它将在商店中设置新令牌,更新组件,从而调用 componentWillReceiveProps . 如果它仍然没有取出(不确定这个检查是否必要),它将加载东西 .

  • 26

    我围绕 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刷新令牌工作流程

相关问题