it-swarm.com.de

Axios Interceptors wiederholen die ursprüngliche Anforderung und greifen auf das ursprüngliche Versprechen zu

Ich habe einen Interceptor, der 401 Fehler abfängt, wenn das Zugriffstoken abläuft. Wenn es abläuft, versucht es das Aktualisierungstoken, um ein neues Zugriffstoken abzurufen. Wenn während dieser Zeit weitere Anrufe getätigt werden, werden sie in die Warteschlange gestellt, bis das Zugriffstoken überprüft wird.

Das funktioniert alles sehr gut. Bei der Verarbeitung der Warteschlange mit Axios (originalRequest) werden jedoch die ursprünglich angefügten Zusagen nicht aufgerufen. Siehe unten für ein Beispiel.

Arbeitender Interceptor-Code:

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)
    }
  }
)

Aktualisierungsmethode (verwendet das Aktualisierungstoken, um ein neues Zugriffstoken zu erhalten)

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.')
  })
},

Methode im Modul auth/actions abonnieren:

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

Sowie die Mutation:

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

Hier ist eine Beispielaktion:

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

Wenn diese Anforderung der Warteschlange hinzugefügt wurde, weil das Aktualisierungstoken auf ein neues Token zugreifen musste, würde ich das Original then () anhängen.

  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)
  })

Nachdem das Zugriffstoken aktualisiert wurde, wird die Warteschlange der Anforderungen verarbeitet:

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

Update 13. Februar 2019

Da sich viele Leute für dieses Thema interessiert haben, habe ich das Paket axios-auth-refresh erstellt, das Ihnen helfen soll, das in diesem Thema angegebene Verhalten zu erreichen.


Der Schlüssel hier ist, das korrekte Promise-Objekt zurückzugeben, sodass Sie .then() für die Verkettung verwenden können. Wir können den Zustand von Vuex dafür verwenden. Wenn der Aktualisierungsaufruf geschieht, können wir nicht nur den refreshing-Status auf true setzen. Wir können den Aktualisierungsaufruf auch auf den ausstehenden Status setzen. Auf diese Weise wird die Verwendung von .then() immer an das richtige Promise-Objekt gebunden und ausgeführt, wenn die Promise abgeschlossen ist. Auf diese Weise benötigen Sie keine zusätzliche Abfrage, um Ihre Anrufe zu behalten, die auf die Aktualisierung gewartet haben.

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;
}

Dies würde immer entweder eine bereits erstellte Anfrage als Promise zurückgeben oder die neue Anfrage erstellen und für die anderen Anrufe speichern. Nun würde Ihr Abfangjäger dem folgenden ähnlich aussehen.

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);
});

Auf diese Weise können Sie alle ausstehenden Anforderungen erneut ausführen. Aber alles auf einmal, ohne zu fragen. 


Wenn Sie möchten, dass ausstehende Anforderungen in der Reihenfolge ausgeführt werden, in der sie tatsächlich aufgerufen wurden, müssen Sie den Rückruf als zweiten Parameter an die Funktion refreshToken() übergeben.

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;
}

Und der Abfangjäger:

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);
});

Ich habe das zweite Beispiel nicht getestet, aber es sollte funktionieren oder zumindest eine Idee geben.

Arbeitsdemo des ersten Beispiels - Aufgrund der Mock-Anfragen und der verwendeten Demo-Version des Dienstes wird es nach einiger Zeit nicht funktionieren. Der Code ist jedoch noch vorhanden.

Quelle: Interceptors - Verhindern, dass abgefangene Nachrichten als Fehler behoben werden

18
Dawid Zbiński

Warum versuchst du nicht so etwas?

Hier verwende ich AXIOS-Interzeptoren in beide Richtungen. Für die abgehende Richtung setze ich den Authorization-Header. Für die eingehende Richtung - wenn ein Fehler vorliegt, gebe ich ein Versprechen zurück (und AXIOS wird versuchen, es zu lösen). Das Versprechen prüft, was der Fehler war - wenn es 401 war und wir es zum ersten Mal sehen (d. H. Wir befinden uns nicht im Wiederholungsversuch), versuche ich, das Token zu aktualisieren. Andernfalls werfe ich den ursprünglichen Fehler ... In meinem Fall verwendet refreshToken() AWS Cognito, aber Sie können verwenden, was Ihnen am meisten zusagt. Hier habe ich 2 Rückrufe für refreshToken():

  1. wenn das Token erfolgreich aktualisiert wurde, wiederhole ich die AXIOS-Anforderung mit einer aktualisierten Konfiguration - einschließlich des neuen frischen Token und dem Setzen eines retry-Flags, damit wir keinen Endloszyklus eingeben, wenn die API wiederholt mit 401 Fehlern reagiert. Wir müssen die Argumente resolve und reject an AXIOS übergeben, andernfalls wird unser neues Versprechen niemals gelöst/abgelehnt.

  2. wenn das Token aus irgendeinem Grund nicht aktualisiert werden konnte, lehnen wir das Versprechen ab. Wir können nicht einfach einen Fehler auslösen, da es möglicherweise einen try/catch-Block um den Rückruf in AWS Cognito gibt


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;
    });
  }
); 
1
IVO GELOV