Axios Interceptors повторите первоначальный запрос и получите доступ к оригинальному обещанию

У меня есть перехватчик на месте, чтобы поймать 401 ошибки, если истекает токен доступа. Если он истекает, он пытается обновить токен, чтобы получить новый токен доступа. Если в течение этого времени выполняются любые другие вызовы, они ставятся в очередь до тех пор, пока токен доступа не будет проверен.

Все это работает очень хорошо. Однако при обработке очереди с использованием Axios (originalRequest) первоначально вложенные обещания не вызываются. Ниже приведен пример.

Рабочий код перехватчика:

Axios.interceptors.response.use(
  response => response,
  (error) => {
    const status = error.response ? error.response.status : null
    const originalRequest = error.config

    if (status === 401) {
      if (!store.state.auth.isRefreshing) {
        store.dispatch('auth/refresh')
      }

      const retryOrigReq = store.dispatch('auth/subscribe', token => {
        originalRequest.headers['Authorization'] = 'Bearer ' + token
        Axios(originalRequest)
      })

      return retryOrigReq
    } else {
      return Promise.reject(error)
    }
  }
)

Метод обновления (используется токен обновления для получения нового токена доступа)

refresh ({ commit }) {
  commit(types.REFRESHING, true)
  Vue.$http.post('/login/refresh', {
    refresh_token: store.getters['auth/refreshToken']
  }).then(response => {
    if (response.status === 401) {
      store.dispatch('auth/reset')
      store.dispatch('app/error', 'You have been logged out.')
    } else {
      commit(types.AUTH, {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token
      })
      store.dispatch('auth/refreshed', response.data.access_token)
    }
  }).catch(() => {
    store.dispatch('auth/reset')
    store.dispatch('app/error', 'You have been logged out.')
  })
},

Метод Subscribe в модуле auth/actions:

subscribe ({ commit }, request) {
  commit(types.SUBSCRIBEREFRESH, request)
  return request
},

Как и Мутация:

[SUBSCRIBEREFRESH] (state, request) {
  state.refreshSubscribers.push(request)
},

Вот пример действия:

Vue.$http.get('/users/' + rootState.auth.user.id + '/tasks').then(response => {
  if (response && response.data) {
    commit(types.NOTIFICATIONS, response.data || [])
  }
})

Если этот запрос был добавлен в очередь I, потому что токен обновления должен был получить доступ к новому токену, я хотел бы добавить оригинал then():

  const retryOrigReq = store.dispatch('auth/subscribe', token => {
    originalRequest.headers['Authorization'] = 'Bearer ' + token
    // I would like to attache the original .then() as it contained critical functions to be called after the request was completed. Usually mutating a store etc...
    Axios(originalRequest).then(//if then present attache here)
  })

После обновления токена доступа очередь запросов обрабатывается:

refreshed ({ commit }, token) {
  commit(types.REFRESHING, false)
  store.state.auth.refreshSubscribers.map(cb => cb(token))
  commit(types.CLEARSUBSCRIBERS)
},

Ответ 1

Обновление от 13 февраля 2019 г.

Поскольку многие люди проявляют интерес к этой теме, я создал пакет axios-auth-refresh, который должен помочь вам добиться поведения, указанного здесь.


Ключевым моментом здесь является возвращение правильного объекта Promise, поэтому вы можете использовать .then() для создания цепочки. Мы можем использовать состояние Vuex для этого. Если происходит вызов на обновление, мы можем не только установить состояние refreshing на true, мы также можем установить ожидающий вызов на обновление. Таким образом, использование .then() всегда будет привязано к нужному объекту Promise и будет выполнено после выполнения Promise. Это гарантирует, что вам не понадобится дополнительная очередь для хранения вызовов, ожидающих обновления токена.

function refreshToken(store) {
    if (store.state.auth.isRefreshing) {
        return store.state.auth.refreshingCall;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(true);
    });
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

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

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store).then(_ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

Это позволит вам выполнить все ожидающие запросы еще раз. Но все сразу, без каких-либо запросов.


Если вы хотите, чтобы ожидающие запросы выполнялись в том порядке, в котором они действительно были вызваны, вам нужно передать обратный вызов в качестве второго параметра функции refreshToken(), например, так:

function refreshToken(store, cb) {
    if (store.state.auth.isRefreshing) {
        const chained = store.state.auth.refreshingCall.then(cb);
        store.commit('auth/setRefreshingCall', chained);
        return chained;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(token);
    }).then(cb);
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

И перехватчик:

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store, _ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

Я не тестировал второй пример, но он должен сработать или, по крайней мере, дать вам представление.

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

Источник: Перехватчики - как предотвратить перехват перехваченных сообщений как ошибку

Ответ 2

Почему бы не попробовать что-то подобное?

Здесь я использую перехватчики AXIOS в обоих направлениях. Для исходящего направления я установил заголовок Authorization. Для входящего направления - если есть ошибка, я возвращаю обещание (и AXIOS попытается устранить его). Обещание проверяет, в чем заключалась ошибка - если это было 401, и мы видим это в первый раз (то есть мы не внутри повторной попытки), то я пытаюсь обновить токен. В противном случае выкидываю оригинальную ошибку. В моем случае refreshToken() использует AWS Cognito, но вы можете использовать все, что вам больше подходит. Здесь у меня есть 2 обратных вызова для refreshToken():

  1. Когда токен успешно обновляется, я повторяю запрос AXIOS, используя обновленную конфигурацию, включая новый свежий токен и устанавливая флаг retry чтобы мы не вводили бесконечный цикл, если API повторно отвечает 401 ошибками. Мы должны передать аргументы resolve и reject AXIOS, иначе наше новое обещание никогда не будет разрешено/отклонено.

  2. если токен не может быть обновлен по какой-либо причине - мы отклоняем обещание. Мы не можем просто выдать ошибку, потому что может быть блок try/catch вокруг обратного вызова внутри AWS Cognito


Vue.prototype.$axios = axios.create(
  {
    headers:
      {
        'Content-Type': 'application/json',
      },
    baseURL: process.env.API_URL
  }
);

Vue.prototype.$axios.interceptors.request.use(
  config =>
  {
    events.$emit('show_spin');
    let token = getTokenID();
    if(token && token.length) config.headers['Authorization'] = token;
    return config;
  },
  error =>
  {
    events.$emit('hide_spin');
    if (error.status === 401) VueRouter.push('/login'); // probably not needed
    else throw error;
  }
);

Vue.prototype.$axios.interceptors.response.use(
  response =>
  {
    events.$emit('hide_spin');
    return response;
  },
  error =>
  {
    events.$emit('hide_spin');
    return new Promise(function(resolve,reject)
    {
      if (error.config && error.response && error.response.status === 401 && !error.config.__isRetry)
      {
        myVue.refreshToken(function()
        {
          error.config.__isRetry = true;
          error.config.headers['Authorization'] = getTokenID();
          myVue.$axios(error.config).then(resolve,reject);
        },function(flag) // true = invalid session, false = something else
        {
          if(process.env.NODE_ENV === 'development') console.log('Could not refresh token');
          if(getUserID()) myVue.showFailed('Could not refresh the Authorization Token');
          reject(flag);
        });
      }
      else throw error;
    });
  }
);