React native Refresh有效,但下一次调用仍使用最后一个令牌

问题描述 投票:14回答:3

我使用以下中间件在到期时刷新我的令牌:

import {AsyncStorage} from 'react-native';
import moment from 'moment';
import fetch from "../components/Fetch";
import jwt_decode from 'jwt-decode';

/**
 * This middleware is meant to be the refresher of the authentication token, on each request to the API,
 * it will first call refresh token endpoint
 * @returns {function(*=): Function}
 * @param store
 */
const tokenMiddleware = store => next => async action => {
  if (typeof action === 'object' && action.type !== "FETCHING_TEMPLATES_FAILED") {
    let eToken = await AsyncStorage.getItem('eToken');
    if (isExpired(eToken)) {
      let rToken = await AsyncStorage.getItem('rToken');

      let formData = new FormData();
      formData.append("refresh_token", rToken);

      await fetch('/token/refresh',
        {
          method: 'POST',
          body: formData
        })
        .then(response => response.json())
        .then(async (data) => {
            let decoded = jwt_decode(data.token);
            console.log({"refreshed": data.token});

            return await Promise.all([
              await AsyncStorage.setItem('token', data.token).then(() => {return AsyncStorage.getItem('token')}),
              await AsyncStorage.setItem('rToken', data.refresh_token).then(() => {return AsyncStorage.getItem('rToken')}),
              await AsyncStorage.setItem('eToken', decoded.exp.toString()).then(() => {return AsyncStorage.getItem('eToken')}),
            ]).then((values) => {
              return next(action);
            });
        }).catch((err) => {
          console.log(err);
        });

      return next(action);
    } else {
      return next(action);
    }
  }

  function isExpired(expiresIn) {
    // We refresh the token 3.5 hours before it expires(12600 seconds) (lifetime on server  25200seconds)
    return moment.unix(expiresIn).diff(moment(), 'seconds') < 10;
  }
};
  export default tokenMiddleware;

和获取助手:

import { AsyncStorage } from 'react-native';
import GLOBALS from '../constants/Globals';
import {toast} from "./Toast";
import I18n from "../i18n/i18n";

const jsonLdMimeType = 'application/ld+json';

export default async function (url, options = {}, noApi = false) {
  if ('undefined' === typeof options.headers) options.headers = new Headers();
  if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);

  if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
    options.headers.set('Content-Type', jsonLdMimeType);
  }

  let token = await AsyncStorage.getItem('token');
  console.log({"url": url,"new fetch": token});
  if (token) {
    options.headers.set('Authorization', 'Bearer ' + token);
  }

  let api = '/api';

  if (noApi) {
    api = "";
  }

  const link = GLOBALS.BASE_URL + api + url;
  return fetch(link, options).then(response => {
    if (response.ok) return response;

    return response
      .json()
      .then(json => {
        if (json.code === 401) {
          toast(I18n.t(json.message), "danger", 3000);
          AsyncStorage.setItem('token', '');
        }

        const error = json['message'] ? json['message'] : response.statusText;
        throw Error(I18n.t(error));
      })
      .catch(err => {
        throw err;
      });
  })
  .catch(err => {
    throw err;
  });
}

我的问题是:

  • 当我做出动作时,会调用中间件。
  • 如果令牌即将过期,则调用刷新令牌方法并更新AsyncStorage。
  • 然后应该调用next(action)方法。
  • 但是我的/templates端点在使用旧的过期令牌之前(而不是之后)调用我的/token/refresh端点...
  • 然后结果是我当前的屏幕返回错误(未授权),但如果用户更改屏幕,它将再次起作用,因为其令牌已成功刷新。但这样丑陋:p

编辑:为了这个问题,我修改了我的代码,把它放到一个文件中。我还放了一些console.log来显示这段代码的执行方式

Execution queue

我们从图像中可以看出:

  • 我的调用(/ templates)在刷新端点之前执行。我的刷新令牌的控制台日志很久就到了......

对此有何帮助?

编辑直到赏金结束:

从这个问题我试着理解为什么我的方法对中间件是错误的,因为我在互联网上找到的许多资源都将中间件作为实现刷新令牌操作的最佳解决方案。

javascript reactjs react-native jwt bearer-token
3个回答
3
投票

我的处理设置略有不同。我没有在中间件中处理刷新令牌逻辑,而是将其定义为辅助函数。这样我可以在任何我认为合适的网络请求之前完成所有令牌验证,并且任何不涉及网络请求的redux操作都不需要此功能

export const refreshToken = async () => {
  let valid = true;

  if (!validateAccessToken()) {
    try {
      //logic to refresh token
      valid = true;
    } catch (err) {
      valid = false;
    }

    return valid;
  }
  return valid;
};

const validateAccessToken = () => {
  const currentTime = new Date();

  if (
    moment(currentTime).add(10, 'm') <
    moment(jwtDecode(token).exp * 1000)
  ) {
    return true;
  }
  return false;
};

现在我们有了这个辅助函数,我将其称为所需的所有redux操作

const shouldRefreshToken = await refreshToken();
    if (!shouldRefreshToken) {
      dispatch({
        type: OPERATION_FAILED,
        payload: apiErrorGenerator({ err: { response: { status: 401 } } })
      });
    } else { 
      //...
    }

2
投票

在你的中间件中,你使store.dispatch异步,但store.dispatch的原始签名是同步的。这可能会产生严重的副作用。

让我们考虑一个简单的中间件,记录应用程序中发生的每个操作,以及它之后计算的状态:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

编写上述中间件基本上是在执行以下操作:

const next = store.dispatch  // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) {  // you change it to meet your needs
  console.log('dispatching', action)
  let result = next(action) // and you return whatever the current version is supposed to return
  console.log('next state', store.getState())
  return result
}

考虑这个例子,将3个这样的中间件链接在一起:

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => action => {
  console.log("dispatching 2", action);
  let result = next(action);
  console.log("next state 2", store.getState());
  return result;
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

首先logger得到行动,然后logger2,然后logger3然后它去实际的store.dispatch&减速器被调用。 reducer将状态从0更改为1,logger3获得更新状态,并将返回值(操作)传播回logger2然后logger

现在,让我们考虑当您将store.dispatch更改为链中间某处的异步函数时会发生什么:

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(5000).then(v => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

我修改了logger2,但logger(链上的那个)不知道next现在是异步的。它将返回一个挂起的Promise并返回“未更新”状态,因为调度的动作尚未到达减速器。

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action); // will return a pending Promise
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(2000).then(() => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({ // console.log of it's return value is too a pending `Promise`
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

因此,我的store.dispatch立即从中间件链中返回,而未决的Promise和console.log('current state', store.getState());仍然打印0.行动到达原来的store.dispatch和减速器looong之后。


我不知道你的整个设置,但我猜这种情况就像你的情况一样。你假设你的中间件已经完成了某些事情并且进行了往返,但实际上它还没有完成这项工作(或者没有人用awaited他完成它)。可能是您正在派遣一个动作来获取/templates,并且由于您编写了一个中间件来自动更新承载令牌,您假设将使用全新令牌调用fetch helper实用程序。但是dispatch已经提前返回了一个未决的承诺,你的令牌仍然是旧的。

除此之外,只有一件事看起来是错误的:你通过next在中间件中两次发送相同的动作:

const tokenMiddleware = store => next => async action => {
  if (something) {
    if (something) {
      await fetch('/token/refresh',)
        .then(async (data) => {
            return await Promise.all([
              // ...
            ]).then((values) => {
              return next(action); // First, after the `Promise.all` resolves
            });
        });
      return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
    } else {
      return next(action);
    }
  }

建议:

  1. 将您的令牌保存在redux商店中,将其存放在存储中并从存储中重新补充redux存储
  2. 为所有api调用写一个Async Action Creator,必要时刷新令牌并仅在令牌刷新后异步调度操作。

redux thunk的示例:

function apiCallMaker(dispatch, url, actions) {
  dispatch({
    type: actions[0]
  })

  return fetch(url)
    .then(
      response => response.json(),
      error => {
        dispatch({
          type: actions[2],
          payload: error
        })
      }
    )
    .then(json =>
      dispatch({
        type: actions[1],
        payload: json
      })
    )
  }
}

export function createApiCallingActions(url, actions) {
  return function(dispatch, getState) {

    const { accessToken, refreshToken } = getState();
    if(neededToRefresh) {
      return fetch(url)
        .then(
          response => response.json(),
          error => {
            dispatch({
              type: 'TOKEN_REFRESH_FAILURE',
              payload: error
            })
          }
        )
        .then(json =>
          dispatch({
              type: 'TOKEN_REFRESH_SUCCESS',
              payload: json
          })
          apiCallMaker(dispatch, url, actions)
        )
    } else {
      return apiCallMaker(dispatch, url, actions)
    }
}

你会像这样使用它:

dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])

dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])

1
投票

你有一个竞争条件的请求,没有正确的解决方案,将完全解决这个问题。下一个项目可以作为解决此问题的起点:

  • 单独使用令牌刷新并等待其在客户端执行,例如如果在会话超时的一半时间内发送任何请求,则发送令牌刷新(像GET / keepalive一样) - 这将导致所有请求都是100%授权的事实(我肯定会使用的选项 - 它可以是也用于跟踪请求但事件)
  • 收到401后清理令牌 - 重新加载后你将看不到工作应用程序假设在边界场景情况下删除有效令牌是积极的情况(简单实现解决方案)
  • 重复查询收到401有一些延迟(实际上不是最好的选项)
  • 强制令牌更新频率超过超时 - 在超时的50-75%更改它们将减少失败请求的数量(但如果用户是所有会话时间的谜语,它们仍然会持续存在)。因此,任何有效请求都将返回将使用的新有效令牌,而不是旧令牌。
  • 当旧令牌在转移期间被计为有效时实施令牌扩展期 - 旧令牌在一段有限时间内被延长以绕过问题(听起来不是很好但至少是一个选项)
© www.soinside.com 2019 - 2024. All rights reserved.