【问题标题】:Automating access token refreshing via interceptors in axios通过 axios 中的拦截器自动刷新访问令牌
【发布时间】:2019-01-09 19:49:18
【问题描述】:

我们最近在this question 中讨论了用于 OAuth 身份验证令牌刷新的 axios 拦截器。

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

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

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

  1. 检查调用的 URL,如果是 /auth/refresh,则不应尝试刷新令牌;
  2. 在调用refreshToken 逻辑时省略拦截器

第一个选项在我看来有点“不动态”。第二个选项看起来很有希望,但我不确定它是否可能。

那么主要的问题是,我们如何区分/识别拦截器中的调用并为它们运行不同的逻辑而无需专门“硬编码”它,或者有什么方法可以省略指定调用的拦截器?提前谢谢你。

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

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

和令牌刷新部分:

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

【问题讨论】:

    标签: oauth-2.0 oauth axios interceptor


    【解决方案1】:

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

    所以,现在你的普通拦截器将是相同的。

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

    而且,你的 refreshToken 会是这样的:

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

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

    【讨论】:

      【解决方案2】:

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

      代码:

      createAxiosResponseInterceptor() {
          const interceptor = axios.interceptors.response.use(
              response => response,
              error => {
                  // Reject promise if usual error
                  if (error.response.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);
              }
          );
      }
      

      【讨论】:

      • 对不起,但我不知道如何使用该解决方案,请您提供一个提示,说明如何将该功能注入 axios。
      • 当然这意味着在刷新请求之前返回的任何未决请求都不会被拦截器捕获,因此在收到新令牌时不会重试
      • 如何在我的 axios 配置中使用这个功能?
      • 不知道怎么用的,直接调用createAxiosResponseInterceptor()函数就可以了,不用在axios config里改。
      • 使用拦截器:axios.interceptors.response.use(createAxiosResponseInterceptor); 在这里阅读更多:github.com/axios/axios#interceptors
      【解决方案3】:

      在所选择的解决方案中似乎省略了一些内容:如果在刷新期间触发请求会发生什么?为什么要等待令牌过期和 401 响应才能获取新令牌?

      1) 刷新请求被触发

      2) 触发对正常资源的另一个请求

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

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

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

      从这个问题Axios Request Interceptor wait until ajax call finishes 和@waleed-ali 对这个问题的回答看来,请求拦截器可以返回一个 Promise。

      我的解决方案是保留请求并在刷新请求解决后立即触发它们。

      在我的 vuex 商店用户模块中(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())
          }))
        }
      

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

      在 RefreshToken 操作中也是如此,因此会产生一个刷新循环,该循环将在任何 401 发生之前自动刷新令牌。

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

      setRefreshing(Promise.resolve())
      

      当刷新请求被满足时,刷新变量将立即解决。

      还有:

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

      这调用了api/user.ts文件的刷新方法(又调用了axios):

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

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

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

      refresh 变量默认保存一个已解析的 Promise,并在它被触发时设置为挂起的刷新请求。

      然后在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)
        }
      )
      

      如果请求是针对刷新端点的,我们会立即解决它,如果不是,我们会返回刷新承诺,并在我们获得更新后的令牌后将其与拦截器中想要执行的操作链接起来。 如果当前没有挂起的刷新请求,则承诺设置为立即解决,如果有刷新请求,那么我们将等待它解决,然后我们将能够使用新令牌启动所有其他挂起的请求。

      只需将拦截器配置为忽略刷新端点即可改进,但我还不知道该怎么做。

      【讨论】:

      • 我明白你的意思,但在我看来,你实际上是在处理我们甚至没有在这个主题中讨论过的事情。这个问题已经回答了,我认为没有必要讨论刷新本身背后的正确“业务逻辑”。如果您有兴趣,我在构建这个库时专门提出了这个问题:github.com/Flyrell/axios-auth-refresh
      【解决方案4】:

      我正在使用 react 来构建我的应用程序的前端,并经常使用几乎相同的策略来解决此类问题

      #ReactJs #JasvaScript #Axios

      import axios from 'axios';
      
      const baseURL = process.env.REACT_APP_SERVICE_URL;
      
      const service = axios.create({ baseURL });
      
      function saveToken(access_token, refresh_token) {
        sessionStorage.setItem('access_token', access_token);
        sessionStorage.setItem('refresh_token', refresh_token);
      }
      function destroyToken() {
        sessionStorage.removeItem('access_token');
        sessionStorage.removeItem('refresh_token');
      }
      function refresh() {
        return new Promise((resolve, reject) => {
          service.post('/api/v1/refresh', {
            refresh_token: sessionStorage.getItem('refresh_token')
          }).then((response) => {
            saveToken(response.data.access_token, response.data.refresh_token);
            return resolve(response.data.access_token);
          }).catch((error) => {
            destroyToken();
            window.location.replace('/logout');
            return reject(error);
          });
        });
      }
      
      service.interceptors.response.use(
        (res) => res,
        (error) => {
          const status = error.response ? error.response.status : null;
          if (status === 401) {
            window.location.replace('/logout');
            sessionStorage.removeItem('access_token');
            sessionStorage.removeItem('refresh_token');
          }
          // status might be undefined
          if (!status) {
            refresh();
          }
          return Promise.reject(error);
        }
      );
      
      service.interceptors.request.use((config) => {
        const access_token = sessionStorage.getItem('access_token');
        config.headers.Authorization = `Bearer ${access_token}`;
        return config;
      });
      
      export { service };
      

      在我的情况下,有时 error.response 是未定义的,所以这就是为什么我首先更新访问令牌,如果有错误然后注销

      【讨论】:

        【解决方案5】:

        我的实现选项

        ~/main.js

        Vue.use(axiosPlugin, {store})
        

        ~/plugins/axios.js

        import axios from 'axios'
        import JWT from '@/utils/jwt'
        import {AuthService} from '@/services/auth.service'
        
        const UNAUTHORIZED_URLS = [
            '/auth/login',
            '/auth/token/refresh',
            '/user/create'
        ]
        
        export default {
            async requestInterceptor(config) {
        
                /* Token validation and adding Authorization header */
                if (!UNAUTHORIZED_URLS.includes(config.url)) {
                    if (!JWT.validateToken(this.store.getters['auth/accessToken'])) {
                        await this.store.dispatch('auth/refreshUserToken')
                    }
        
                    config.headers['Authorization'] = AuthService.getAuthorizationHeader()
                }
            
                return config
            },
            async responseErrorInterceptor(error) {
                if (![401, 419].includes(error.response.status)) {
                    return Promise.reject(error)
                }
        
                /* If this is a repeated request */
                if (error.response.config._retry) {
                    await this.store.dispatch('auth/logout')
        
                    return Promise.reject(error)
                }
        
                /* Attempting to update the token and retry the request */
                try {
                    this.store.dispatch('auth/refreshUserToken')
        
                    error.response.config._retry = true
        
                    error.response.config.headers['Authorization'] = AuthService.getAuthorizationHeader()
        
                    return axios(error.response.config)
                } catch (error) {
                    await this.store.dispatch('auth/logout')
        
                    return Promise.reject(error)
                }
            },
            install(Vue, options) {
                axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
                axios.interceptors.request.use(this.requestInterceptor.bind(options))
                axios.interceptors.response.use(undefined, this.responseErrorInterceptor.bind(options))
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-11-03
          • 2021-02-11
          • 2020-03-04
          • 2020-01-13
          • 1970-01-01
          • 2019-10-22
          • 2020-12-03
          • 2023-03-06
          相关资源
          最近更新 更多