web-dev-qa-db-fra.com

Automatisation du rafraîchissement des jetons d'accès via des intercepteurs dans axios

Nous avons récemment discuté d'un intercepteur axios pour OAuth rafraîchissement du jeton d'authentification dans cette question .

Fondamentalement, ce que l’intercepteur doit faire est d’intercepter toute réponse avec 401 code d'état et essayez de rafraîchir le jeton. Dans cet esprit, la prochaine chose à faire est de renvoyer une promesse de l'intercepteur, afin que toute demande qui aurait normalement échoué, s'exécute car rien ne se passe après une actualisation de jeton.

Le problème principal est qu'un intercepteur ne vérifie que le 401 code d'état, ce qui n'est pas suffisant, car refreshToken renverra également 401 code d'état en cas d'échec - et nous avons une boucle.

Il y a deux scénarios possibles que j'ai en tête:

  1. vérifiez l'URL appelée, donc si c'est /auth/refresh il ne devrait pas essayer de rafraîchir le jeton;
  2. omettre un intercepteur lorsque la logique refreshToken est appelée

La première option me semble un peu "non dynamique". La deuxième option semble prometteuse, mais je ne sais pas si c'est possible.

La question principale est alors, comment pouvons-nous différencier/identifier les appels dans un intercepteur et exécuter une logique différente pour eux sans le "coder en dur" spécifiquement, ou existe-t-il un moyen d'omettre l'intercepteur pour un appel spécifié? Merci d'avance.

Le code d'un intercepteur pourrait aider à comprendre la question:

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

    if (status === 401) {
        // will loop if refreshToken returns 401
        return refreshToken(store).then(_ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        })
        // Would be Nice to catch an error here, which would work, if the interceptor is omitted
        .catch(err => err);
    }

    return Promise.reject(error);
});

et jeton rafraîchissant:

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;
}
9
Dawid Zbiński

J'ai peut-être trouvé un moyen beaucoup plus simple de gérer cela: utilisez axios.interceptors.response.eject () pour désactiver l'intercepteur lorsque j'appelle le point de terminaison/api/refresh_token et le réactiver après.

Le code :

createAxiosResponseInterceptor() {
    const interceptor = axios.interceptors.response.use(
        response => response,
        error => {
            // Reject promise if usual error
            if (errorResponse.status !== 401) {
                return Promise.reject(error);
            }

            /* 
             * When response code is 401, try to refresh the token.
             * Eject the interceptor so it doesn't loop in case
             * token refresh causes the 401 response
             */
            axios.interceptors.response.eject(interceptor);

            return axios.post('/api/refresh_token', {
                'refresh_token': this._getToken('refresh_token')
            }).then(response => {
                saveToken();
                error.response.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
                return axios(error.response.config);
            }).catch(error => {
                destroyToken();
                this.router.Push('/login');
                return Promise.reject(error);
            }).finally(createAxiosResponseInterceptor);
        }
    );
}
25
Ismoil Shifoev

Je ne sais pas si cela correspond à vos besoins ou non, mais une autre solution pourrait également être les instances Axios distinctes (en utilisant axios.create méthode) pour refreshToken et le reste des appels d'API. De cette façon, vous pouvez facilement contourner votre intercepteur par défaut pour vérifier l'état 401 en cas de refreshToken.

Donc, maintenant votre intercepteur normal serait le même.

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

  if (status === 401) {
    // will loop if refreshToken returns 401
    return refreshToken(store).then(_ => {
      error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
      error.config.baseURL = undefined;
      return Axios.request(error.config);
    })
    // Would be Nice to catch an error here, which would work, if the interceptor is omitted
    .catch(err => err);
  }

  return Promise.reject(error);
});

Et, votre refreshToken serait comme:

const refreshInstance = Axios.create();

function refreshToken(store) {
  if (store.state.auth.isRefreshing) {
    return store.state.auth.refreshingCall;
  }

  store.commit('auth/setRefreshingState', true);
  const refreshingCall = refreshInstance.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;
}

voici quelques liens sympas [1][2] , vous pouvez vous référer aux instances Axios

2
waleed ali

Quelque chose qui semble être omis dans la solution choisie est: que se passe-t-il si une demande est déclenchée pendant l'actualisation? Et pourquoi attendre l'expiration d'un token et une réponse 401 pour obtenir un nouveau token?

1) la demande de rafraîchissement est déclenchée

2) une autre demande de ressource normale est déclenchée

3) réponse de rafraîchissement reçue, le jeton a changé (ce qui signifie que l'ancien jeton n'est pas valide)

4) Back-end traiter la demande de l'étape 2 mais il a reçu l'ancien jeton => 401

Fondamentalement, vous obtiendrez 401 pour toutes les demandes renvoyées lors de la demande de rafraîchissement (du moins, c'est le problème auquel j'ai été confronté).

A partir de cette question Axios Request Interceptor attend la fin de l'appel ajax et de @ waleed-ALi réponse à cette question, il apparaît que les intercepteurs de requête peuvent retourner une promesse.

Ma solution consiste à conserver les demandes et à les déclencher juste après la résolution de la demande d'actualisation.

Dans mon module utilisateur vuex store (vuex + vuex-module-decorators):

  @Action({ rawError: true })
  public async Login(userInfo: { email: string, password: string }) {
    let { email, password } = userInfo
    email = email.trim()
    const { data } = await login({ email, password })
    setToken(data.access_token)
    setTokenExpireTime(Date.now() + data.expires_in * 1000)
    this.SET_TOKEN(data.access_token)
    // after getting a new token, set up the next refresh in 'expires_in' - 10 seconds
    console.log("You've just been logged-in, token will be refreshed in ", data.expires_in * 1000 - 10000, "ms")
    setTimeout(this.RefreshToken, data.expires_in * 1000 - 10000)
  }

  @Action
  public async RefreshToken() {
    setRefreshing(refresh().then(({ data }) => {
      setToken(data.access_token) // this calls a util function to set a cookie
      setTokenExpireTime(Date.now() + data.expires_in * 1000) // same here
      this.SET_TOKEN(data.access_token)
      // after getting a new token, set up the next refresh in 'expires_in' - 10 seconds
      console.log('Token refreshed, next refresh in ', data.expires_in * 1000 - 10000)
      setTimeout(this.RefreshToken, data.expires_in * 1000 - 10000)
      setRefreshing(Promise.resolve())
    }))
  }

Dans l'action de connexion, j'ai configuré un délai pour appeler RefreshToken juste avant l'expiration du jeton.

Idem dans l'action RefreshToken, créant ainsi une boucle de rafraîchissement qui actualisera automatiquement le jeton avant qu'un 401 ne se produise.

Les deux lignes importantes du module Utilisateur sont:

setRefreshing(Promise.resolve())

Lorsque la demande de rafraîchissement est satisfaite, la variable d'actualisation se résout instantanément.

Et:

setRefreshing(refresh().then(({ data }) => {

cela appelle la méthode de rafraîchissement du fichier api/user.ts (qui à son tour appelle axios):

export const refresh = () =>
  request({
    url: '/users/login/refresh',
    method: 'post'
  })

et envoyer la promesse retournée dans la méthode utilitaire setRefreshing dans utils.ts:

let refreshing: Promise<any> = Promise.resolve()
export const getRefreshing = () => refreshing
export const setRefreshing = (refreshingPromise: Promise<any>) => { refreshing = refreshingPromise }

La variable d'actualisation contient une promesse résolue par défaut et sera définie sur la demande d'actualisation en attente lorsqu'elle est déclenchée.

Puis dans request.ts:

    service.interceptors.request.use(
  (config) => {
    if (config.url !== '/users/login/refresh') {
      return getRefreshing().then(() => {
        // Add Authorization header to every request, you can add other custom headers here
        if (UserModule.token) {
          console.log('changing token to:', UserModule.token)
          console.log('calling', config.url, 'now')
          config.headers['Authorization'] = 'Bearer ' + UserModule.token
        }
        return config
      })
    } else {
      return Promise.resolve(config)
    }
  },
  (error) => {
    Promise.reject(error)
  }
)

Si la demande concerne le point de terminaison d'actualisation, nous la résolvons immédiatement, sinon nous renvoyons la promesse d'actualisation et l'enchaînons avec ce que nous voulons faire dans l'intercepteur APRÈS que nous obtenions le jeton mis à jour. S'il n'y a aucune demande d'actualisation actuellement en attente, la promesse est réglée pour se résoudre instantanément, s'il y a une demande d'actualisation, nous attendrons qu'elle soit résolue et nous pourrons lancer toutes les autres demandes en attente avec le nouveau jeton.

Pourrait être amélioré en configurant simplement l'intercepteur pour ignorer le point de terminaison d'actualisation, mais je ne sais pas encore comment le faire.

0
user3803848