Как обрабатывать токен обновления при поступлении нескольких запросов?

Я использую reactjs, mbox и axios и столкнулся с проблемой. У меня есть api, который выдает токен доступа и токен обновления. Токен доступа умирает каждые 20 минут, и когда это происходит, сервер отправляет обратно 401, и мой код автоматически отправляет токен обновления, чтобы получить новый токен доступа.

Как только будет предоставлен новый токен доступа, тот же отклоненный запрос будет отправлен снова. Теперь мой код отлично работает, пока я не выброшу несколько отказов, которые могут сработать одновременно.

Итак, первый запрос уходит, 401 отправляется обратно, и он получает новый токен обновления, ну, все остальные запросы будут пытаться сделать то же самое, но другие запросы теперь будут терпеть неудачу, потому что будет использоваться токен обновления и новый. будет выдан по первому запросу.

Это запустит мой код для перенаправления пользователя на страницу входа.

По сути, я застрял только в одном запросе за раз.

export const axiosInstance = axios.create({
    baseURL: getBaseUrl(),
    timeout: 5000,
    contentType: "application/json",
    Authorization: getAuthToken()
  });

  export function updateAuthInstant() {
    axiosInstance.defaults.headers.common["Authorization"] = getAuthToken();
  }


function getAuthToken() {
    if (localStorage.getItem("authentication")) {
      const auth = JSON.parse(localStorage.getItem("authentication"));
      return `Bearer ${auth.accessToken}`;
    }
  }

axiosInstance.interceptors.response.use(
  function(response) {
    return response;
  },
  function(error) {
    const originalRequest = error.config;
    if (error.code != "ECONNABORTED" && error.response.status === 401) {
      if (!originalRequest._retry) {
        originalRequest._retry = true;
        return axiosInstance
          .post("/tokens/auth", {
            refreshToken: getRefreshToken(),
            grantType: "refresh_token",
            clientId : "myclient"
          })
          .then(response => {

            uiStores.authenticaionUiStore.setAuthentication(JSON.stringify(response.data))
            updateAuthInstant();
            return axiosInstance(originalRequest);
          });
      } else {
        uiStores.authenticaionUiStore.logout();
        browserHistory.push({ pathname: '/login',});
      }

    }
    return Promise.reject(error);
  }
);

Редактировать

У меня проблема с тем, что код, который мне нужно проверить для сброса аутентификации, не работает, когда пользователь копирует прямой URL-адрес

app.js

  <React.Fragment>
       <Switch>
          <Route path="/members" component={MemberAreaComponent} />
        </Switch>
  </React.Fragment >

В memberAreaComponent

      <Route path="/members/home" component={MembersHomeComponent} />

Когда я набираю http://www.mywebsite/members/home

MembersHomeComponent - componentDidMount runs first
MemberAreaComponent - componentDidMount runs second
AppCoontainer = componentDidMount runs last.

person chobo2    schedule 19.08.2018    source источник


Ответы (1)


Привет, я реализовал тот же сценарий в приложении response / redux. Но это поможет вам достичь цели. Вам не нужно проверять 401 при каждом вызове API. Просто реализуйте его в своем первом запросе API проверки. Вы можете использовать setTimeOut для отправки запроса API обновления токена до истечения срока действия токена аутентификации. Таким образом, locatStorage будет обновляться, и все запросы axios никогда не получат токены с истекшим сроком действия. Вот мое решение:

в моем Constants.js Я поддерживаю ТОКЕН ПОЛЬЗОВАТЕЛЯ в localStorage следующим образом:

 export const USER_TOKEN = {
   set: ({ token, refreshToken }) => {
      localStorage.setItem('access_token', token);
      localStorage.setItem('refresh_token', refreshToken);
   },
   remove: () => {
      localStorage.removeItem('access_token');
      localStorage.removeItem('refresh_token');
 },
   get: () => ({
     agent: 'agent',
     token: localStorage.getItem('access_token'),
     refreshToken: localStorage.getItem('refresh_token'),
  }),
   get notEmpty() {
      return this.get().token !== null;
  },
};

export const DEFAULT_HEADER = {
     get: () => ({
      'Content-type': 'application/json;charset=UTF-8',
       agent: `${USER_TOKEN.get().agent}`,
       access_token: `${USER_TOKEN.get().token}`,
 }),
};

при загрузке страницы запрос API проверки пользователем выглядит следующим образом:

dispatch(actions.validateUser(userPayload)) // First time authentication with user credentials and it return access token, refresh token and expiry time
  .then(userData => {
    const { expires_in, access_token, refresh_token } = userData
    USER_TOKEN.set({          // setting tokens in localStorage to accessible to all API calls
      token: access_token,
      refreshToken: refresh_token,
    });
    const timeout = expires_in * 1000 - 60 * 1000; // you can configure as you want but here it is 1 min before token will get expired
    this.expiryTimer = setTimeout(() => {  // this would reset localStorage before token expiry timr
      this.onRefreshToken();
    }, timeout);
  }).catch(error => {
    console.log("ERROR", error)
  });

onRefreshToken = () => {
   const { dispatch } = this.props;
   const refresh_token = USER_TOKEN.get().refreshToken;
   dispatch(actions.refreshToken({ refresh_token })).then(userData => {
      const { access_token, refresh_token } = userData
      USER_TOKEN.set({
         token: access_token,
          refreshToken: refresh_token,
    });
  });
};

Не стесняйтесь задавать любые вопросы. Другой способ - реализовать контроллер прерывания axios для отмены ожидающих обещаний. Рад помочь и с этим!

EDITED - вы можете поддерживать источник токена axios во всех ваших запросах API, чтобы прервать их в любое время. поддерживать источник токенов axios во всех своих API. как только первое обещание будет выполнено, вы можете отменить все остальные ожидающие запросы API. Вы можете вызвать метод onAbort после того, как будет выполнено ваше первое обещание. Видеть это:

//in your component
class MyComponent extends Component{
isTokenSource = axios.CancelToken.source(); // a signal you can point to any API

componentDidMount{
   // for example if you're sending multiple api call here
        this.props.dispatch(actions.myRequest(payload, this.isTokenSource.token))
        .then(() => {
            // all good
        })
        .catch(error => {
            if (axios.isCancel(error)) {
                console.warn('Error', error);
            }
        });
}

onAbortStuff = () => {  // cancel request interceptor
    console.log("Aborting Request");
    this.isTokenSource.cancel('API was cancelled'); // This will abort all the pending promises if you send the same token in multiple requests, 
}

render(){
//
}

В своем запросе axios вы можете отправить токен следующим образом:

export const myRequest= (id, cancelToken) => {
    const URL = `foo`;
    return axios(URL, {
      method: 'GET',
      headers: DEFAULT_HEADER.get(),
      cancelToken: cancelToken
    })
.then(response => {
  // handle success
  return response.data;
  })
.catch(error => {
  throw error;
   });
  };

Для справки вы можете прочитать эту статью, она очень полезна для понимания отмены подписок. https://medium.freecodecamp.org/how-to-work-with-react-the-right-way-to-avoid-some-common-pitfalls-fc9eb5e34d9e

Вы можете структурировать свои маршруты таким образом: index.js

<Provider store={store}>
  <BrowserRouter>
    <App />
  </BrowserRouter>
</Provider>

App.js:

class App extends Component {


state = {
    isAuthenticated: false,
  };

  componentDidMount() {
   //authentication API and later you can setState isAuthenticate
   }
    render() {
    const { isAuthenticated } = this.state;
    return isAuthenticated ? <Routes /> : <Loading />;
  }

Если вы все еще обнаружите какую-либо проблему, я более чем счастлив помочь вам с этим.

person Sakhi Mansoor    schedule 19.08.2018
comment
Хм, интересно, подумал, я подумал, что вы воспользуетесь интервальным таймером, так как, может быть, я его не вижу, но как вы держите свой таймер? Я думаю, поскольку токены доступа принимаются до тех пор, пока срок их действия истек, даже если новый токен создается, а старый отправляется, он все равно будет пропущен? Как ваш код поступает, если говорят, что токен обновления был недействителен, и вам действительно нужно вытеснить их со своего сайта? Все еще проверяете 401? - person chobo2; 20.08.2018
comment
Если это не так много, чтобы показать, я хотел бы увидеть, как будет работать прерывание, так как я думаю, если вы прервете их все, вы действительно повторно отправите запросы, чтобы был создан новый токен? - person chobo2; 20.08.2018
comment
Я хотел бы ответить на оба ваших вопроса; - Timer = ›Есть какая-то реализация на бэкэнде, я получаю оставшееся время истечения срока действия этого токена в ответ на API проверки. Даже приложение перезагружается, и оно снова отправляет запрос API проверки и получает оставшееся время истечения срока действия, поскольку мы управляем временем истечения срока действия в бэкэнде. Поэтому я установил SetTImeOut за 1 минуту до фактического срока годности. Он принимает токен обновления, и я получаю новый токен доступа и токен обновления. Затем я обновляю locatStoarge. Таким образом, все API используют последний токен доступа в своих заголовках. Вам не нужно проверять 401 во всех API. - person Sakhi Mansoor; 20.08.2018
comment
На ваш второй вопрос относительно отмены запросов вы можете увидеть мой ОТРЕДАКТИВНЫЙ ответ выше. - person Sakhi Mansoor; 20.08.2018
comment
Интересно. Итак, вы выбрали вариант 1, верно? Почему? Для первого способа вы все еще проверяете 401 для перенаправления для входа в систему? - person chobo2; 20.08.2018
comment
Да, я выбрал вариант 1. Я считаю, что это более разумный подход, просто чтобы прояснить ситуацию. Нет, я проверяю только 400 на наличие неверных запросов для перехода к входу в систему для неавторизованных пользователей. Мой случай отличается. Я использую веб-просмотр в Android и iOS, и я получаю первый токен доступа из собственного приложения при каждой перезагрузке приложения. Оба подхода хороши. Я бы посоветовал выбрать вариант, который больше подходит для вашего случая. Если у вас есть какие-либо вопросы, вы можете связаться со мной в любое время. - person Sakhi Mansoor; 20.08.2018
comment
Я тоже выбрал вариант 1. У меня просто возникают проблемы, когда говорят, что они закрывают браузер, а затем возвращаются по скопированной ссылке. Поэтому я поместил свой код, чтобы проверить, есть ли у них аутентификация в моем app.js. Я получаю перенаправление на свой логин в качестве компонента, который они собираются запустить до установки app.js, поэтому происходит ajax, и он возвращается с 401. Мне нужно какое-то событие, которое запускается раньше всего. - person chobo2; 31.08.2018
comment
@ chobo2 у вас должен быть родительский компонент для всех ваших маршрутов, который должен быть установлен в первую очередь, и вы должны проверить аутентификацию в componentDidMount. Я делаю это прямо сейчас - person Sakhi Mansoor; 31.08.2018
comment
Что ж, app.js - это мой корневой компонент, но у меня есть несколько других компонентов, в которых есть маршруты (компонент-член, компонент администратора). Так, может быть, мне нужно добавить свой код к каждому из них? - person chobo2; 31.08.2018
comment
хм, я смотрю свой код. У меня есть MembersArea, у которого есть маршрут к моему разделу администратора, но когда я напрямую ввожу URL-адрес в область администратора, компонент администратора запускается до компонентаmberarea - person chobo2; 31.08.2018
comment
@ chobo2 На всех маршрутах должен быть компонент выше, который будет отображаться первым, если вы загрузите приложение с любым URL-адресом. - person Sakhi Mansoor; 31.08.2018
comment
Что ж, это должен быть мой app.js, так как это первое, что я делаю ‹Provider routingStore = {routingStore} domainStores = {DomainStores} uiStores = {uiStores}› ‹История маршрутизатора = {history}› ‹AppContainer /› ‹/Router› ‹/Provider›, - person chobo2; 31.08.2018
comment
Позвольте нам продолжить это обсуждение в чате. - person Sakhi Mansoor; 31.08.2018