web-dev-qa-db-fra.com

Axios Interceptors réessaie la demande initiale et accède à la promesse initiale

J'ai un intercepteur en place pour capturer les erreurs 401 si le jeton d'accès expire. S'il expire, il essaie le jeton d'actualisation pour obtenir un nouveau jeton d'accès. Si d'autres appels sont effectués pendant cette période, ils sont mis en file d'attente jusqu'à la validation du jeton d'accès.

Cela fonctionne très bien. Toutefois, lors du traitement de la file d'attente à l'aide d'Axios (originalRequest), les promesses attachées à l'origine ne sont pas appelées. Voir ci-dessous pour un exemple.

Code d'interception de travail:

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

Méthode d'actualisation (utilisé le jeton d'actualisation pour obtenir un nouveau jeton d'accès)

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

Méthode Subscribe dans le module auth/actions:

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

En plus de la mutation:

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

Voici un exemple d'action:

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

Si cette demande a été ajoutée à la file d'attente I parce que le jeton d'actualisation devait accéder à un nouveau jeton, j'aimerais joindre l'original 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)
  })

Une fois le jeton d'accès actualisé, la file d'attente des demandes est traitée:

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

Mise à jour du 13 février 2019

Comme de nombreuses personnes ont manifesté de l'intérêt pour ce sujet, j'ai créé le paquet axios-auth-refresh qui devrait vous aider à adopter le comportement spécifié dans ce sujet.


La clé ici est de renvoyer l'objet Promise correct afin que vous puissiez utiliser .then() pour le chaînage. Nous pouvons utiliser l'état de Vuex pour cela. Si l'appel d'actualisation se produit, nous pouvons non seulement définir l'état refreshing sur true, nous pouvons également définir l'appel d'actualisation sur celui en attente. De cette façon, l'utilisation de .then() sera toujours liée au bon objet Promise et sera exécutée une fois la promesse faite. De cette façon, vous n'avez besoin d'aucune requête supplémentaire pour conserver vos appels en attente d'actualisation.

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

Cela renverrait toujours soit une demande déjà créée sous forme de promesse, soit en créerait une nouvelle et l'enregistrerait pour les autres appels. Votre intercepteur ressemblerait maintenant au suivant.

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

Cela vous permettra d'exécuter à nouveau toutes les demandes en attente. Mais tout à la fois, sans interrogation. 


Si vous souhaitez que les demandes en attente soient exécutées dans l'ordre dans lequel elles ont été appelées, vous devez transmettre le rappel en tant que second paramètre à la fonction refreshToken(), comme suit.

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

Et l'intercepteur:

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

Je n'ai pas testé le deuxième exemple, mais cela devrait fonctionner ou au moins vous donner une idée.

Démonstration de travail du premier exemple - en raison des demandes fictives et de la version de service de démonstration utilisée, elles ne fonctionneront pas après un certain temps, mais le code est toujours là.

Source: Intercepteurs - Comment empêcher les messages interceptés de se transformer en erreur

18
Dawid Zbiński

Pourquoi ne pas essayer quelque chose comme ça?

Ici, j'utilise des intercepteurs AXIOS dans les deux sens. Pour la direction sortante, j'ai défini l'en-tête Authorization. Pour la direction entrante - s'il y a une erreur, je retourne une promesse (et AXIOS essaiera de la résoudre). La promesse vérifie l’erreur: si c’est 401 et que nous la voyons pour la première fois (c’est-à-dire que nous ne sommes pas à l’essai), j’essaie d’actualiser le jeton. Sinon, je renvoie l'erreur d'origine . Dans mon cas, refreshToken() utilise AWS Cognito, mais vous pouvez utiliser celui qui vous convient le mieux. Ici, j'ai 2 rappels pour refreshToken():

  1. lorsque le jeton est actualisé avec succès, je réessaie la demande AXIOS en utilisant une configuration mise à jour, y compris le nouveau jeton récent et en définissant un indicateur retry pour ne pas entrer dans un cycle sans fin si l'API répond de manière répétée avec 401 erreurs. Nous devons transmettre les arguments resolve et reject à AXIOS, sans quoi notre nouvelle promesse ne sera jamais résolue/rejetée.

  2. si le jeton ne peut pas être actualisé pour une raison quelconque, nous rejetons la promesse. Nous ne pouvons pas simplement renvoyer une erreur car il pourrait y avoir un bloc try/catch autour du rappel dans 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;
    });
  }
); 
1
IVO GELOV