redux 刷新令牌中间件

Posted

技术标签:

【中文标题】redux 刷新令牌中间件【英文标题】:redux refresh token middleware 【发布时间】:2018-03-27 07:49:05 【问题描述】:

我有一个中间件,它可以在下一个操作运行之前转到刷新令牌,然后在访问令牌过期时运行另一个操作。

但是,如果我一次发出多个请求并且访问令牌结束,我将尝试获取与我请求一样多的刷新令牌。我正在检查状态中的 isLoading 属性以防止这种情况发生。但是请求之后,reducer中的isLoading值是true,中间件好像是false,所以反复请求。

我在 fetching_refresh_token 操作中发送 refreshTokenPromise,但我从未收到 state.refreshTokenPromise,它始终未定义。

我的状态肯定有问题。

所以这是我的问题,如何访问中间件中不断变化的状态值?

刷新令牌中间件:(此版本多次命中端点)

import  AsyncStorage  from 'react-native';
import  MIN_TOKEN_LIFESPAN  from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import 
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE  from '../actions/constants';

export default function tokenMiddleware( dispatch, getState ) 
  return next => async (action) => 
    if (typeof action === 'function') 
      const state = getState();
      if (state) 
        const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
        if (expiresIn && isExpired(JSON.parse(expiresIn))) 
          if (!state.refreshToken.isLoading) 
            return refreshToken(dispatch).then(() => next(action));
          
          return state.refreshTokenPromise.then(() => next(action));
        
      
    
    return next(action);
  ;


async function refreshToken(dispatch) 
  const clientId = await AsyncStorage.getItem('CLIENT_ID');
  const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
  const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');

  const userObject = 
    grant_type: 'refresh_token',
    client_id: JSON.parse(clientId),
    client_secret: JSON.parse(clientSecret),
    refresh_token: refreshToken1,
  ;

  const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');

  const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => 
    await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
    await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
    await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));

    dispatch(
      type: FETCHING_REFRESH_TOKEN_SUCCESS,
      data: res,
    );

    return res ? Promise.resolve(res) : Promise.reject(
      message: 'could not refresh token',
    );
  ).catch((err) => 
    dispatch(
      type: FETCHING_REFRESH_TOKEN_FAILURE,
    );

    throw err;
  );

  dispatch(
    type: FETCHING_REFRESH_TOKEN,
    refreshTokenPromise,
  );

  return refreshTokenPromise;


function isExpired(expiresIn) 
  return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;

刷新令牌缩减器:

import 
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE  from '../actions/constants';

const initialState = 
  token: [],
  isLoading: false,
  error: false,
;

export default function refreshTokenReducer(state = initialState, action) 
  switch (action.type) 
    case FETCHING_REFRESH_TOKEN:
      return 
        ...state,
        token: [],
        isLoading: true,
      ;
    case FETCHING_REFRESH_TOKEN_SUCCESS:
      return 
        ...state,
        isLoading: false,
        token: action.data,
      ;
    case FETCHING_REFRESH_TOKEN_FAILURE:
      return 
        ...state,
        isLoading: false,
        error: true,
      ;
    default:
      return state;
  

与此同时,当我将它发送到 getState 到 refreshToken 函数时,我会在 refreshToken 中获得不断变化的状态值。但在这个版本中,刷新令牌会在不刷新的情况下用于其他操作。

Monkey Patched 版本:(此版本只发出 1 个请求)

import  AsyncStorage  from 'react-native';
import  MIN_TOKEN_LIFESPAN  from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import 
  FETCHING_REFRESH_TOKEN,
  FETCHING_REFRESH_TOKEN_SUCCESS,
  FETCHING_REFRESH_TOKEN_FAILURE  from '../actions/constants';

export default function tokenMiddleware( dispatch, getState ) 
  return next => async (action) => 
    if (typeof action === 'function') 
      const state = getState();
      if (state) 
        const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
        if (expiresIn && isExpired(JSON.parse(expiresIn))) 
          if (!state.refreshTokenPromise) 
            return refreshToken(dispatch, getState).then(() => next(action));
          
          return state.refreshTokenPromise.then(() => next(action));
        
      
    
    return next(action);
  ;


async function refreshToken(dispatch, getState) 
  const clientId = await AsyncStorage.getItem('CLIENT_ID');
  const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
  const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');

  const userObject = 
    grant_type: 'refresh_token',
    client_id: JSON.parse(clientId),
    client_secret: JSON.parse(clientSecret),
    refresh_token: refreshToken1,
  ;

  if (!getState().refreshToken.isLoading) 
    const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');

    const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => 
      await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
      await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
      await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));

      dispatch(
        type: FETCHING_REFRESH_TOKEN_SUCCESS,
        data: res,
      );

      return res ? Promise.resolve(res) : Promise.reject(
        message: 'could not refresh token',
      );
    ).catch((err) => 
      dispatch(
        type: FETCHING_REFRESH_TOKEN_FAILURE,
      );

      throw err;
    );

    dispatch(
      type: FETCHING_REFRESH_TOKEN,
      refreshTokenPromise,
    );

    return refreshTokenPromise;
  


function isExpired(expiresIn) 
  return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;

谢谢。

【问题讨论】:

我看不到你在哪里检查中间件中的 isLoading 值.. 我尝试了其他的东西,修好了,你会再看一遍吗? 当然!我很好奇 你看了吗? 看起来不错,不过我觉得如果你想写得恰到好处,还是试试中间件github.com/redux-saga/redux-saga吧。 【参考方案1】:

你可以从 redux-sagas 中受益

https://github.com/redux-saga/redux-saga

redux-sagas 只是后台运行程序,它监视您的操作并在遇到某些特定操作时做出反应。您可以监听所有动作并对所有动作做出反应,也可以只对 cme​​ts 中提到的最新动作作出反应

https://redux-saga.js.org/docs/api/#takelatestpattern-saga-args

虽然 redux-thunk 只是在移动中创建操作并等待一些 I/O 发生然后在 I/O 完成时创建更多操作的另一种方式。它更像是同步代码模式,而 redux-sagas 更像是多线程。在主线程上,您的应用程序正在运行,而在后台线程上,您有 sagas 监视器和反应

【讨论】:

在尝试实施 sagas 以执行 OP 讨论的内容时遇到问题。似乎我必须在每个生成器函数中实现这个? OP的答案放在API axios中间件上不是更好吗? 你可以用几个 sagas 对同一个事件做出反应。因此,如果您愿意,您可以拥有对所有 sagas 做出反应的功能。在此处查看基本记录器示例:redux-saga.js.org/docs/advanced/FutureActions.html 简而言之:如果要使用此功能执行所有操作,只需使用takeEvery('*', function* something(action)...)。您也可以只拥有 x y z 操作所需的功能,然后将其添加到 root saga 描述中。无需在多个 saga 处理程序中重复。你也可以用高阶函数做一些事情。谷歌关于那些如果不知道的话。【参考方案2】:

我使用 axios 中间件解决了这个问题。我觉得挺好看的。

import  AsyncStorage  from 'react-native';
import Config from 'react-native-config';
import axios from 'axios';
import  store  from '../store';
import  refreshToken  from '../actions/refreshToken'; // eslint-disable-line

const instance = axios.create(
  baseURL: Config.API_URL,
);

let authTokenRequest;

function resetAuthTokenRequest() 
  authTokenRequest = null;


async function getAuthToken() 
  const clientRefreshToken = await AsyncStorage.getItem('clientRefreshToken');

  if (!authTokenRequest) 
    authTokenRequest = store.dispatch(refreshToken(clientRefreshToken));

    authTokenRequest.then(
      () => 
        const 
          token:  payload ,
         = store.getState();

        // save payload to async storage
      ,
      () => 
        resetAuthTokenRequest();
      ,
    );
  

  return authTokenRequest;


instance.interceptors.response.use(
  response => response,
  async (error) => 
    const originalRequest = error.config;

    if (
      error.response.status === 401
      && !originalRequest._retry // eslint-disable-line no-underscore-dangle
    ) 
      return getAuthToken()
        .then(() => 
          const 
            token: 
              payload:  'access-token': accessToken, client, uid ,
            ,
           = store.getState();

          originalRequest.headers['access-token'] = accessToken;
          originalRequest.headers.client = client;
          originalRequest.headers.uid = uid;
          originalRequest._retry = true; // eslint-disable-line no-underscore-dangle

          return axios(originalRequest);
        )
        .catch(err => Promise.reject(err));
    

    return Promise.reject(error);
  ,
);

export default instance;

如果您有问题,请不要犹豫。

【讨论】:

您好,您是在哪里添加的?

以上是关于redux 刷新令牌中间件的主要内容,如果未能解决你的问题,请参考以下文章

使用 redux saga 在 react native 中实现令牌刷新

快速中间件中的 Node.js JWT 刷新令牌

Laravel JWT 刷新令牌中间件和并发请求

黑名单/验证/生成 JWT 刷新令牌

Laravel 护照刷新令牌

如何结束用户会话令牌并刷新令牌,时间超过 25 分钟