关于vue.js:通过axios中的拦截器自动刷新访问令牌

Automating access token refreshing via interceptors in axios

我们最近在此问题中讨论了用于AAuth身份验证令牌刷新的axios拦截器。

基本上,拦截器应该做的是拦截状态码为401的任何响应,并尝试刷新令牌。
考虑到这一点,接下来要做的是从拦截器返回一个Promise,这样任何通常会失败的请求都将在令牌刷新后没有任何反应的情况下运行。

主要的问题是,拦截器仅检查401状态码,这是不够的,因为refreshToken也会在失败时返回401状态码-我们有一个循环。

我想到了两种可能的情况:

  • 检查被调用的URL,因此如果它是/auth/refresh,则不应尝试刷新令牌;
  • 调用refreshToken逻辑时省略拦截器
  • 第一个选项对我来说有点"不动态"。第二种选择看起来很有希望,但是我不确定是否有可能。

    然后的主要问题是,我们如何才能在一个拦截器中区分/识别调用并为它们运行不同的逻辑,而无需对其进行"硬编码",或者有什么方法可以为指定的调用省略拦截器?预先谢谢您。

    拦截器的代码可能有助于理解以下问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Axios.interceptors.response.use(respon> response, err> {
        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(e> err);
        }

        return Promise.reject(error);
    });

    和令牌刷新部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;
    }


    我可能已经找到一种更简单的方法来处理此问题:当我调用/ api / refresh_token端点并在之后重新启用它时,请使用axios.interceptors.response.eject()禁用拦截器。

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    createAxiosResponseInterceptor() {
        const interceptor = axios.interceptors.response.use(
            response p;gt; response,
            error p;gt; {
                // 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 p;gt; {
                    saveToken();
                    error.response.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
                    return axios(error.response.config);
                }).catch(error p;gt; {
                    destroyToken();
                    this.router.push('/login');
                    return Promise.reject(error);
                }).finally(createAxiosResponseInterceptor);
            }
        );
    }


    不确定是否满足您的要求,但另一个解决方法也可以是单独的Axios实例(使用axios.create方法)来进行refreshToken和其余API调用。这样,在refreshToken的情况下,您可以轻松地绕过默认拦截器来检查401状态。

    因此,现在您的普通拦截器将是相同的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Axios.interceptors.response.use(response =;gt; response, error =;gt; {
      const status = error.response ? error.response.status : null

      if (status === 401) {
        // will loop if refreshToken returns 401
        return refreshToken(store).then(_ =;gt; {
          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 =;gt; err);
      }

      return Promise.reject(error);
    });

    而且,您的refreshToken将类似于:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 } }) =;gt; {
        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;
    }

    这里有一些不错的链接[1] [2],您可以参考Axios实例


    在所选解决方案中似乎可以忽略的事情是:如果刷新期间触发了请求,会发生什么?为什么还要等待令牌过期和401响应才能获取新令牌?

    1)触发刷新请求

    2)触发了另一个对普通资源的请求

    3)收到刷新响应,令牌已更改(意味着旧令牌无效)

    4)后端处理来自步骤2的请求,但收到旧令牌=> 401

    基本上,刷新请求期间触发的所有请求都将得到401(至少这是我一直面临的问题)。

    从此问题开始,Axios请求拦截器将一直等到ajax调用结束为止,然后从@ waleed-ali对此问题进行回答,看来请求拦截器可以返回Promise。

    我的解决方案所做的是保留请求,并在刷新请求得到解决后立即将其触发。

    在我的vuex商店中的用户模块(vuex vuex-module-decorators):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
      @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 })mp;gt; {
          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())
        }))
      }

    在"登录"操作中,我设置了一个超时时间,以在令牌到期之前立即调用RefreshToken。

    与RefreshToken操作相同,因此创建了一个刷新循环,该刷新循环将在出现任何401之前自动刷新令牌。

    用户模块的两个重要行是:

    1
    setRefreshing(Promise.resolve())

    满足刷新请求后,刷新变量将立即解析。

    并且:

    1
    setRefreshing(refresh().then(({ data })mp;gt; {

    这将调用api / user.ts文件的刷新方法(依次称为axios):

    1
    2
    3
    4
    5
    export const refresh = ()mp;gt;
      request({
        url: '/users/login/refresh',
        method: 'post'
      })

    并将返回的Promise发送到utils.ts中的setRefreshing实用程序方法中:

    1
    2
    3
    let refreshing: Promimp;lt;amp;gt; = Promise.resolve()
    export const getRefreshing = ()mp;gt; refreshing
    export const setRefreshing = (refreshingPromise: Promimp;lt;amp;gt;)mp;gt; { refreshing = refreshingPromise }

    默认情况下,刷新变量包含已解析的Promise,并且在触发后将被设置为待处理的刷新请求。

    然后在request.ts中输入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
        service.interceptors.request.use(
      (config)mp;gt; {
        if (config.url !== '/users/login/refresh') {
          return getRefreshing().then(()mp;gt; {
            // 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)mp;gt; {
        Promise.reject(error)
      }
    )

    如果请求是针对刷新端点的,则我们将立即解决它,如果不是,我们将返回刷新承诺,并将其与我们要在拦截器中执行的操作链接起来,之后我们将获取更新的令牌。
    如果当前没有任何待处理的刷新请求,则将诺言设置为立即解决;如果有刷新请求,则我们将等待其解决,并能够使用新令牌启动所有其他待处理请求。铅>

    可以通过仅将拦截器配置为忽略刷新端点来进行改进,但是我还不知道该怎么做。