简体   繁体   中英

How to setup Axios interceptors with React Context properly?

Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.

The problem is, this creates a closure and returns old data to the interceptor when it's being called.

I am using JWT authentication using React/Node and I'm storing access tokens using Context API.

This is how my Interceptor component looks like right now:

import React, { useEffect, useContext } from 'react';
import { Context } from '../../components/Store/Store';
import { useHistory } from 'react-router-dom';
import axios from 'axios';

const ax = axios.create();

const Interceptor = ({ children }) => {
  const [store, dispatch] = useContext(Context);

  const history = useHistory();

  const getRefreshToken = async () => {
    try {
      if (!store.user.token) {
        dispatch({
            type: 'setMain',
            loading: false,
            error: false,
            auth: store.main.auth,
            brand: store.main.brand,
            theme: store.main.theme,
          });

        const { data } = await axios.post('/api/auth/refresh_token', {
          headers: {
            credentials: 'include',
          },
        });

        if (data.user) {
          dispatch({
            type: 'setStore',
            loading: false,
            error: false,
            auth: store.main.auth,
            brand: store.main.brand,
            theme: store.main.theme,
            authenticated: true,
            token: data.accessToken,
            id: data.user.id,
            name: data.user.name,
            email: data.user.email,
            photo: data.user.photo,
            stripeId: data.user.stripeId,
            country: data.user.country,
            messages: {
              items: [],
              count: data.user.messages,
            },
            notifications:
              store.user.notifications.items.length !== data.user.notifications
                ? {
                    ...store.user.notifications,
                    items: [],
                    count: data.user.notifications,
                    hasMore: true,
                    cursor: 0,
                    ceiling: 10,
                  }
                : {
                    ...store.user.notifications,
                    count: data.user.notifications,
                  },
            saved: data.user.saved.reduce(function (object, item) {
              object[item] = true;
              return object;
            }, {}),
            cart: {
              items: data.user.cart.reduce(function (object, item) {
                object[item.artwork] = true;
                return object;
              }, {}),
              count: Object.keys(data.user.cart).length,
            },
          });
        } else {
          dispatch({
            type: 'setMain',
            loading: false,
            error: false,
            auth: store.main.auth,
            brand: store.main.brand,
            theme: store.main.theme,
          });
        }
      }
    } catch (err) {
      dispatch({
        type: 'setMain',
        loading: false,
        error: true,
        auth: store.main.auth,
        brand: store.main.brand,
        theme: store.main.theme,
      });
    }
  };

  const interceptTraffic = () => {
     ax.interceptors.request.use(
        (request) => {
            request.headers.Authorization = store.user.token
              ? `Bearer ${store.user.token}`
              : '';

            return request;
          },
        (error) => {
          return Promise.reject(error);
        }
      );

      ax.interceptors.response.use(
        (response) => {
          return response;
        },
        async (error) => {
          console.log(error);
          if (error.response.status !== 401) {
            return new Promise((resolve, reject) => {
              reject(error);
            });
          }

          if (
            error.config.url === '/api/auth/refresh_token' ||
            error.response.message === 'Forbidden'
          ) {
            const { data } = await ax.post('/api/auth/logout', {
              headers: {
                credentials: 'include',
              },
            });
            dispatch({
              type: 'resetUser',
            });
            history.push('/login');

            return new Promise((resolve, reject) => {
              reject(error);
            });
          }

          const { data } = await axios.post(`/api/auth/refresh_token`, {
            headers: {
              credentials: 'include',
            },
          });

          dispatch({
            type: 'updateUser',
            token: data.accessToken,
            email: data.user.email,
            photo: data.user.photo,
            stripeId: data.user.stripeId,
            country: data.user.country,
            messages: { items: [], count: data.user.messages },
            notifications:
              store.user.notifications.items.length !== data.user.notifications
                ? {
                    ...store.user.notifications,
                    items: [],
                    count: data.user.notifications,
                    hasMore: true,
                    cursor: 0,
                    ceiling: 10,
                  }
                : {
                    ...store.user.notifications,
                    count: data.user.notifications,
                  },
            saved: data.user.saved,
            cart: { items: {}, count: data.user.cart },
          });

          const config = error.config;
          config.headers['Authorization'] = `Bearer ${data.accessToken}`;

          return new Promise((resolve, reject) => {
            axios
              .request(config)
              .then((response) => {
                resolve(response);
              })
              .catch((error) => {
                reject(error);
              });
          });
        }
      );
  };

  useEffect(() => {
    getRefreshToken();
    if (!store.main.loading) interceptTraffic();
  }, []);

  return store.main.loading ? 'Loading...' : children;
}

export { ax };
export default Interceptor;

The getRefreshToken function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.

The interceptTraffic function is where the issue persists. It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.

You will notice that I am exporting ax (an instance of Axios where I added interceptors) but when it's being called outside this component, it references old store data due to closure.

This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.

Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.

Any help is appreciated, thanks.

Common Approach (localStorage)

It is a common practice to store the JWT in the localStorage with

localStorage.setItem('token', 'your_jwt_eykdfjkdf...');

on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage

custom-axios.js

import axios from 'axios';

// axios instance for making requests 
const axiosInstance = axios.create();

// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
  // add token to request headers
  config.headers['Authorization'] = localStorage.getItem('token');
  return config;
});

export default axiosInstance;

And then, just import the Axios instance we just created and make requests.

import axios from './custom-axios';

axios.get('/url');
axios.post('/url', { message: 'hello' });

Another approach (when you've token stored in the state)

If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:

custom-axios.js

import axios from 'axios';

const customAxios = (token) => {
  // axios instance for making requests
  const axiosInstance = axios.create();

  // request interceptor for adding token
  axiosInstance.interceptors.request.use((config) => {
    // add token to request headers
    config.headers['Authorization'] = token;
    return config;
  });

  return axiosInstance;
};

export default customAxios;

And then import the function we just created, grab the token from state, and make requests:

import axios from './custom-axios';

// logic to get token from state (it may vary from your approach but the idea is same)
const token = useSelector(token => token);

axios(token).get('/url');
axios(token).post('/url', { message: 'hello' });

I have a template that works in a system with millions of access every day.

This solved my problems with refresh token and reattemp the request without crashing

First I have a "api.js" with axios, configurations, addresses, headers. In this file there are two methods, one with auth and another without. In this same file I configured my interceptor:

import axios from "axios";
import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService";
    
export const api = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
        "Content-Type": "application/json",
    },
});

export const apiSecure = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
        Authorization: "Bearer " + localStorage.getItem("Token"),
        "Content-Type": "application/json",
    },
    
    export default api;
    
    apiSecure.interceptors.response.use(
        function (response) {
            return response;
        },
        function (error) {
            const access_token = localStorage.getItem("Token");
            if (error.response.status === 401 && access_token) {
                return ResetTokenAndReattemptRequest(error);
            } else {
                console.error(error);
            }
            return Promise.reject(error);
        }
    );

Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:

import api from "../api";
import axios from "axios";

let isAlreadyFetchingAccessToken = false;

let subscribers = [];

export async function ResetTokenAndReattemptRequest(error) {
  try {
    const { response: errorResponse } = error;
    const retryOriginalRequest = new Promise((resolve) => {
      addSubscriber((access_token) => {
        errorResponse.config.headers.Authorization = "Bearer " + access_token;
        resolve(axios(errorResponse.config));
      });
    });
    if (!isAlreadyFetchingAccessToken) {
      isAlreadyFetchingAccessToken = true;
      await api
        .post("/Auth/refresh", {
          Token: localStorage.getItem("RefreshToken"),
          LoginProvider: "Web",
        })
        .then(function (response) {
          localStorage.setItem("Token", response.data.accessToken);
          localStorage.setItem("RefreshToken", response.data.refreshToken);
          localStorage.setItem("ExpiresAt", response.data.expiresAt);
        })
        .catch(function (error) {
          return Promise.reject(error);
        });
      isAlreadyFetchingAccessToken = false;
      onAccessTokenFetched(localStorage.getItem("Token"));
    }
    return retryOriginalRequest;
  } catch (err) {
    return Promise.reject(err);
  }
}

function onAccessTokenFetched(access_token) {
  subscribers.forEach((callback) => callback(access_token));
  subscribers = [];
}

function addSubscriber(callback) {
  subscribers.push(callback);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM