【问题标题】:How to setup Axios interceptors with React Context properly?如何正确设置带有 React Context 的 Axios 拦截器?
【发布时间】:2020-09-19 02:36:12
【问题描述】:

由于我想使用 React Context 设置 Axios 拦截器,唯一可行的解​​决方案是创建一个 Interceptor 组件,以便使用 useContext 挂钩访问 Context 状态和调度。

问题是,这会创建一个闭包,并在拦截器被调用时将旧数据返回给它。

我正在使用 React/Node 进行 JWT 身份验证,并且我正在使用 Context API 存储访问令牌。

这就是我的拦截器组件现在的样子:

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;

如果 cookie 中有刷新令牌,则每次用户刷新网站以检索访问令牌时调用 getRefreshToken 函数。

interceptTraffic 函数是问题仍然存在的地方。 它由一个请求拦截器和一个响应拦截器组成,请求拦截器为每个请求附加一个带有访问令牌的标头,以及一个响应拦截器,用于处理访问令牌过期,以便使用刷新令牌获取新的。

您会注意到我正在导出 ax(我添加了拦截器的 Axios 的一个实例),但是当它在此组件之外被调用时,由于关闭,它引用了旧的存储数据。

这显然不是一个好的解决方案,但这就是为什么我需要帮助组织拦截器,同时仍然能够访问上下文数据。

请注意,我将此组件创建为包装器,因为它呈现提供给它的子组件,这是主 App 组件。

感谢任何帮助,谢谢。

【问题讨论】:

  • 其实我觉得你用的方法还不错。如果你想让代码更简洁,你可以将一些逻辑(对象映射、Axios 请求)提取到不同的函数中。通常,您作为 Provider 的 Auth 中间件将完成这项工作!
  • 您最终找到了一个好的解决方案吗?我有一个类似的问题......但由于某种原因,我从我的上下文中获取 accessToken,有时我做对了,有时我得到它 NULL,我完全不明白

标签: javascript reactjs axios jwt jwt-auth


【解决方案1】:

常用方法(localStorage)

将 JWT 存储在 localStorage 中是一种常见的做法

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

在登录或页面刷新时,制作一个模块来导出带有令牌的 Axios 实例。我们将从 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;

然后,只需导入我们刚刚创建的 Axios 实例并发出请求。

import axios from './custom-axios';

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

另一种方法(当您将令牌存储在状态中时)

如果您将 JWT 存储在 state 中,或者您可以从 state 中获取新的令牌,请创建一个导出函数的模块,该函数将令牌作为参数并返回带有令牌的 axios 实例,如下所示:

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;

然后导入我们刚刚创建的函数,从状态中获取token,发出请求:

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

【讨论】:

  • token 存储在 state 中时,还有其他方法吗?我不觉得这种方式是优化的。例如:如果我有多个进行 API 调用的服务(不同的文件),我将需要检索每个服务中的状态并将其作为参数传递给实例。有没有办法让它“全球化”?
  • @EstebanChornet 你应该只保留那些应该改变的状态,否则 props/default-props 是最好的解决方案。由于我们不知道令牌何时更改状态,因此我们无法将其设为全局。但是,如果你想让它在你的状态中的令牌发生变化时全局运行,并将令牌设置在本地存储中。这样本地存储将始终包含最新的令牌。然后你可以使用上面的第一种方法^useEffect(() => { localStorage.setItem('token', 'newToken')) }, [tokenInState])
  • 例如,如果我们在网络上思考,我会在未选中“记住我”复选框的情况下进行登录。所以对你来说,最好的方法是将用户的令牌存储在会话存储中并从中获取令牌,而不是在状态中获取它?我将其存储在状态中以管理身份验证流程。从存储中获取令牌不会有更大的性能成本吗?我昨天添加了这个 stackoverflow 问题。如果你看一下,我将不胜感激:-) stackoverflow.com/questions/67772582/…
【解决方案2】:

我有一个模板可以在每天有数百万次访问的系统中运行。

这解决了我的刷新令牌问题并重新尝试请求而不会崩溃

首先,我有一个带有 axios、配置、地址、标头的“api.js”。 在这个文件中,有两种方法,一种使用 auth,另一种没有。 在同一个文件中,我配置了我的拦截器:

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

然后是 ResetTokenAndReattemptRequest 方法。我把它放在另一个文件中,但你可以把它放在任何你想要的地方:

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

【讨论】:

    猜你喜欢
    • 2021-06-16
    • 2022-01-17
    • 2021-02-07
    • 2020-01-19
    • 1970-01-01
    • 2018-11-27
    • 1970-01-01
    • 2019-05-23
    • 1970-01-01
    相关资源
    最近更新 更多