通过axios中的拦截器自动刷新访问令牌

时间:2018-08-02 06:28:14

标签: vue.js oauth oauth-2.0 axios interceptor

我们最近在this question中讨论了用于AAuth身份验证令牌刷新的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;
}

5 个答案:

答案 0 :(得分:11)

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

代码:

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

答案 1 :(得分:2)

不确定是否满足您的要求,但另一个解决方法也可以是单独的Axios实例(使用axios.create方法)来进行refreshToken和其余API调用。这样,您可以轻松地绕过默认拦截器,以防出现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实例

答案 2 :(得分:0)

在所选解决方案中似乎可以忽略的事情是:如果刷新期间触发了请求,会发生什么?为什么还要等待令牌过期和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 }

默认情况下,刷新变量包含已解析的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)
  }
)

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

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

答案 3 :(得分:0)

我正在使用 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 是未定义的,所以这就是为什么我首先更新访问令牌,如果有错误然后注销

答案 4 :(得分:0)

我的实施方案

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