React Native Refresh可以工作,但是下一次调用仍使用最后一个令牌

时间:2019-02-19 15:51:44

标签: javascript reactjs react-native jwt bearer-token

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

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

从图像中我们可以看到:

  • 在刷新端点之前执行了我的呼叫(/模板)。然后刷新令牌的控制台日志到了很久……

对此有任何帮助吗?

编辑直到赏金结束:

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

3 个答案:

答案 0 :(得分: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 { 
      //...
    }

答案 1 :(得分: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/redux@4.0.1/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/redux@4.0.1/dist/redux.js"></script>

因此我的store.dispatch 从中间件链中立即返回,并带有待处理的Promise,并且console.log('current state', store.getState());仍显示0。操作到达原始store.dispatch和减速器紧接着。


我不知道您的整个设置,但是我的猜测是您的情况正在发生。您假设您的中间件已经完成了一些工作并进行了往返,但是实际上它还没有完成工作(或者没有人await让他完成工作)。可能是您正在分派一个操作来提取/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'])

答案 2 :(得分:1)

您的请求处于竞争状态,没有正确的解决方案可以完全解决此问题。下一项可以用作解决此问题的起点:

  • 分别使用令牌刷新并等待其在客户端执行,例如如果在会话超时的一半时间内发送了任何请求,则发送令牌刷新(类似于GET / keepalive之类的消息)-这将导致以下事实:所有请求都将被100%授权(我肯定会使用的选项-可以不仅用于跟踪请求,还用于跟踪事件)
  • 在收到401之后清理令牌-假设边界情况下删除有效令牌是积极的情况,则重新加载后您将看不到正在运行的应用程序
  • 重复查询,但延迟接收到401(实际上不是最佳选择)
  • 强制令牌更新的频率要比超时更新的频率高-在超时的50%至75%的范围内更改令牌会减少失败请求的数量(但如果用户在整个会话时间内处于中间状态,这些请求仍将保留)。因此,任何有效的请求都将返回将使用的新的有效令牌,而不是旧的令牌。

  • 可以执行的令牌扩展期,可以认为旧令牌在传输期间是有效的-为了避免问题而将旧令牌扩展了有限的时间(听起来不是很好,但是至少可以选择)< / p>