如何使用Redux刷新JWT令牌?

lan*_*nan 43 javascript jwt reactjs react-native redux

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

例如

componentDidMount() {
    dispath(loadProfile());
    dispatch(loadAssets());
    ...
}
Run Code Online (Sandbox Code Playgroud)

双方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);
};
Run Code Online (Sandbox Code Playgroud)

}

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

lan*_*nan 36

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

我最初的想法仍然存在:JWT刷新是在中间件中.thunk如果thunk使用中间件必须先到来.

...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);
Run Code Online (Sandbox Code Playgroud)

然后在中间件代码中,我们检查令牌是否在任何异步操作之前过期.如果它已过期,我们还会检查我们是否已经刷新了令牌 - 为了能够进行这样的检查,我们将新的令牌的承诺添加到州.

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);
    };
}
Run Code Online (Sandbox Code Playgroud)

最重要的部分是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;
}
Run Code Online (Sandbox Code Playgroud)

我意识到这很复杂.我也有点担心调度refreshToken行动本身不是行动.请告诉我您使用redux处理JWT令牌过期的任何其他方法.

  • @Shvetusya我不担心在refreshToken中调度动作本身不是动作.refreshToken本质上是一个动作创建者,并在actionCreator中调度其他动作,这是非常常见的做法 (5认同)
  • 美丽的 !对于带有“redux-persist”的人有一点注意,不支持持久化承诺,必须使用变压器将“freshTokenPromise”排除/列入黑名单 (3认同)

Zek*_*oid 18

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

样品减速剂

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

现在是动作创建者:

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());
       }
    };
}
Run Code Online (Sandbox Code Playgroud)

安装组件时会调用此方法.如果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()
    } 
}
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢!这对于初始加载绝对有意义。但我不确定它是否适用于在应用程序加载并使用后过期令牌。每次调用 API 都需要有效的令牌。我们有许多需要登录和加载数据的弹出视图,所以我不确定通过这些视图的 props 处理过期是否有效。 (2认同)
  • 我的第一个问题是为每个需要JWT的动作添加`dispatch({type:'LOAD_FETCHING',fetching:true})`是代码重复.第二个问题是如何知道刷新何时完成.假设有一个"添加到收藏夹"按钮,用于调度需要身份验证的api调用.我是否要添加"如果令牌过期刷新然后拨打电话"逻辑到该操作?其他类似行为怎么样?这就是我尝试使用中间件的原因.在其他框架/语言中我使用了装饰器,但我不确定我是否可以使用React. (2认同)

kmm*_*vnr 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();
Run Code Online (Sandbox Code Playgroud)

我在状态中保留令牌,并使用简单的帮助程序将Acess令牌注入请求标头

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

所以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
      ]
  }
})
Run Code Online (Sandbox Code Playgroud)

我写了这篇文章并分享了项目示例,该示例显示了JWT刷新令牌工作流程的实际应用