Automating access token refreshing via interceptors in axios

吃可爱长大的小学妹 提交于 2019-11-30 07:12:11
Ismoil Shifoev

I may have found a way much simpler to handle this : use axios.interceptors.response.eject() to disable the interceptor when I call the /api/refresh_token endpoint, and re-enable it after.

The 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);
        }
    );
}
waleed ali

Not sure if this suits your requirements or not, but another workaround could also be the separate Axios Instances (using axios.create method) for refreshToken and the rest of API calls. This way you can easily bypass your default interceptor for checking the 401 status in case of refreshToken.

So, now your normal interceptor would be the same.

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

And, your refreshToken would be like:

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

here are some nice links [1] [2], you can refer for Axios Instances

Something that seems to be omitted in the chosen solution is: what happens if a request is triggered during the refresh? And why wait for a token to expire and a 401 response to get a new token?

1) refresh request is triggered

2) another request for a normal resource is triggered

3) refresh response received, token has changed (meaning old token is invalid)

4) Back-end process the request from step 2 but it received the old token => 401

Basically you will get 401 for all request fired during the refresh request (at least that is the issue I've been facing).

From this question Axios Request Interceptor wait until ajax call finishes and from @waleed-ali answer to this question it appears that request interceptors can return a Promise.

What my solution does is holding the requests and fire them right after the refresh request has been resolved.

In my vuex store User module (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())
    }))
  }

In the Login action, I set up a timeout to call RefreshToken just before token will expire.

Same in the RefreshToken action, thus making a refresh loop which will automatically refresh the token before any 401 will ever take place.

The two important lines of the User module are:

setRefreshing(Promise.resolve())

When the refresh request is fulfilled then refreshing variable will instantly resolve.

And:

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

this calls the refresh method of the api/user.ts file (which in turn calls axios) :

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

and send the returned Promise into setRefreshing utility method in utils.ts:

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

The refreshing variable holds a resolved Promise by default and will be set to the pending refresh request when it is fired.

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

If the request is for the refresh endpoint we resolve it at once, if not we return the refreshing promise and chain it with what we want to do in the interceptor AFTER we get the updated token. If there are no refresh request currently pending then the promise is set to resolve instantly, if there is a refresh request then we'll wait for it to resolve and we'll be able to launch all other pending request with the new token.

Could be improved by just configuring the interceptor to ignore the refresh endpoint but I don't know how to do that yet.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!