You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Axios拦截器导航触发循环:跳转登录后返回需多次点击返回键

Axios拦截器跳转登录页后返回键需多次点击的问题解决

我用Axios拦截器处理请求时校验用户登录状态,未登录就跳转到对应登录页,但现在有个问题:用户被跳转到登录页后,得点击返回键3-4次才能回到之前的页面,明明每次操作只发了一次请求。

当前拦截器代码

const usePrivateAxios = () => {
  const navigate = useNavigate();
  const location = useLocation();

  const currentPath = location.pathname;

  useEffect(() => {
    const requestInterceptor = privateAxios.interceptors.request.use(
      (config) => {
        const accessToken = localStorage.getItem("accessToken");
        const user = JSON.parse(sessionStorage.getItem("profile"));

        if (!accessToken || !user) {
          if (currentPath.includes("/admin")) {
            navigate("/admin/login");
          } else if (currentPath.includes("/manager")) {
            navigate("/manager/login");
          } else {
            navigate("/login");
          }

          toast.error(
            <p className="pr-2">Vui lòng đăng nhập trước!</p>,
            toastOptions,
          );
        } else {
          config.headers.Authorization = `Bearer ${accessToken}`;
          sessionStorage.removeItem("entryMessage");
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    const responseInterceptor = privateAxios.interceptors.response.use(
      (response) => {
        const user = JSON.parse(sessionStorage.getItem("profile"));

        if (response.data.status === 401 || response.data.status === 403) {
          if (response.data.status === 401) {
            toast.error(
              <p className="pr-2">Vui lòng đăng nhập trước!</p>,
              toastOptions,
            );
          } else if (response.data.status === 403) {
            toast.error(
              <p className="pr-2">Tài khoản không có quyền truy cập!</p>,
              toastOptions,
            );
          }

          if (user && user.role && user.role.roleName === "ROLE_ADMIN") {
            navigate("/admin/login");
          } else if (
            user &&
            user.role &&
            user.role.roleName === "ROLE_MANAGER"
          ) {
            navigate("/manager/login");
          } else {
            navigate("/login");
          }
        }
        return response;
      },
      (error) => {
        return Promise.reject(error);
      },
    );

    return () => {
      privateAxios.interceptors.request.eject(requestInterceptor);
      privateAxios.interceptors.response.eject(responseInterceptor);
    };
  }, [navigate]);
  return privateAxios;
};

补充:拦截器处理点赞请求示例

// API
export const likePost = async (postId, config) => {
    try {
        const response = await privateAxios.post(`/posts/${postId}/like`, config);
        return response.data;
    } catch (error) {
        throw error;
    }
};

// Func
const handlePostLike = async () => {
    if (post && post.my) {
        toast.error(<p className="pr-2">Bạn không thể thích/bỏ thích bài đăng của mình!</p>, toastOptions);
    } else {
        try {
            const response = await likePost(postId);
            if (response.status === 200) {
                getPostDetail();
            }
        catch (error) {
            navigate("/error-500");
        }
    }
};

问题原因

  1. 拦截器重复注册:usePrivateAxios每次调用或组件重渲染时,都会注册新的拦截器,旧拦截器未被正确清理,导致多次触发跳转,路由栈累积多条登录页记录。
  2. 跳转未终止请求:请求拦截器判断未登录后,未终止请求流程,后续逻辑可能重复触发跳转。
  3. 路由跳转方式错误:用默认的navigate会添加新路由记录,而非替换当前记录,导致路由栈变长。

修复方案

const usePrivateAxios = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const interceptorsRegistered = useRef(false); // 标记拦截器是否已注册

  useEffect(() => {
    if (interceptorsRegistered.current) return;

    const requestInterceptor = privateAxios.interceptors.request.use(
      (config) => {
        const accessToken = localStorage.getItem("accessToken");
        const user = JSON.parse(sessionStorage.getItem("profile"));
        const currentPath = location.pathname;

        if (!accessToken || !user) {
          // 跳转时替换当前路由,避免路由栈累积
          const targetLoginPath = currentPath.includes("/admin") 
            ? "/admin/login" 
            : currentPath.includes("/manager") 
              ? "/manager/login" 
              : "/login";
          
          navigate(targetLoginPath, { replace: true });
          toast.error(
            <p className="pr-2">Vui lòng đăng nhập trước!</p>,
            toastOptions,
          );
          return Promise.reject(new Error("未登录,已跳转")); // 终止请求
        } else {
          config.headers.Authorization = `Bearer ${accessToken}`;
          sessionStorage.removeItem("entryMessage");
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    const responseInterceptor = privateAxios.interceptors.response.use(
      (response) => {
        const user = JSON.parse(sessionStorage.getItem("profile"));

        if (response.data.status === 401 || response.data.status === 403) {
          const targetLoginPath = user?.role?.roleName === "ROLE_ADMIN"
            ? "/admin/login"
            : user?.role?.roleName === "ROLE_MANAGER"
              ? "/manager/login"
              : "/login";
          
          navigate(targetLoginPath, { replace: true });
          toast.error(
            response.data.status === 401 
              ? <p className="pr-2">Vui lòng đăng nhập trước!</p> 
              : <p className="pr-2">Tài khoản không có quyền truy cập!</p>,
            toastOptions,
          );
          return Promise.reject(new Error(response.data.status === 401 ? "未登录" : "无权限"));
        }
        return response;
      },
      (error) => {
        // 处理后端直接返回HTTP 401/403的情况
        if (error.response?.status === 401 || error.response?.status === 403) {
          const user = JSON.parse(sessionStorage.getItem("profile"));
          const targetLoginPath = user?.role?.roleName === "ROLE_ADMIN"
            ? "/admin/login"
            : user?.role?.roleName === "ROLE_MANAGER"
              ? "/manager/login"
              : "/login";
          
          navigate(targetLoginPath, { replace: true });
          toast.error(
            error.response.status === 401 
              ? <p className="pr-2">Vui lòng đăng nhập trước!</p> 
              : <p className="pr-2">Tài khoản không có quyền truy cập!</p>,
            toastOptions,
          );
        }
        return Promise.reject(error);
      },
    );

    interceptorsRegistered.current = true;

    return () => {
      privateAxios.interceptors.request.eject(requestInterceptor);
      privateAxios.interceptors.response.eject(responseInterceptor);
      interceptorsRegistered.current = false; // 卸载时重置标记
    };
  }, [navigate, location.pathname]);

  return privateAxios;
};

核心修复点

  • 拦截器去重:用useRef标记拦截器注册状态,避免重复添加拦截器导致多次跳转。
  • 替换路由跳转:navigate时添加replace: true,直接替换当前路由记录,不再新增条目。
  • 终止未登录请求:判断未登录后返回Promise.reject(),阻止请求继续执行,避免触发后续逻辑。
  • 完善错误处理:同时覆盖后端返回response.data.status和HTTP状态码两种401/403场景。

内容的提问来源于stack exchange,提问作者Văn Thuận Nguyễn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 11:50:55