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

NextJS中Axios(兼容前后端)+TanStack Query鉴权最佳实践

NextJS 客户端/服务端兼容的Axios鉴权+TanStack Query最佳实践

一、解决next/headers导入报错的方案

报错根源是next/headers属于服务端专属API,不能在pages目录的客户端组件或通用文件中直接导入。解决思路是拆分工具代码,严格区分服务端/客户端环境的依赖导入:

1. 拆分Cookie工具类

将服务端和客户端的Cookie读取逻辑分开,避免客户端代码引入服务端依赖:

服务端Cookie工具(server/cookie.ts,仅服务端使用)

import { cookies } from "next/headers";

export const getServerCookieByName = (name: string) => {
  const cookieStore = cookies();
  const cookie = cookieStore.get(name);
  return cookie?.value;
};

客户端Cookie工具(utils/cookie.ts,客户端使用)

import Cookies from "js-cookie";

export const getClientCookieByName = (name: string) => {
  return Cookies.get(name);
};

2. 修改Axios实例,兼容双环境

通过typeof window === 'undefined'判断运行环境,动态导入服务端工具,避免客户端加载服务端依赖:

// utils/axiosInstance.ts
import axios from "axios";
import { getClientCookieByName } from "./cookie";
import Cookies from "js-cookie";

const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;

// 服务端工具仅在服务端环境导入
let getServerCookieByName: (name: string) => string | undefined;
if (typeof window === "undefined") {
  ({ getServerCookieByName } = require("@/server/cookie"));
}

const axiosInstance = axios.create({
  baseURL,
  timeout: 10000,
});

// 请求拦截器:自动填充鉴权头
axiosInstance.interceptors.request.use(async (config) => {
  let authToken: string | undefined;
  let deviceToken: string | undefined;

  // 区分环境获取凭证
  if (typeof window === "undefined") {
    // 服务端:从服务端Cookie读取
    authToken = getServerCookieByName("AUTH");
    deviceToken = getServerCookieByName("DEVICE");
  } else {
    // 客户端:从浏览器Cookie读取
    authToken = getClientCookieByName("AUTH");
    deviceToken = getClientCookieByName("DEVICE");
  }

  // 无DEVICE_TOKEN时自动获取
  if (!deviceToken) {
    // 用原生axios发起请求,避免触发自身拦截器导致循环
    const res = await axios.get(`${BASE_URL}/auth/device-token`);
    deviceToken = res.data.accessToken;

    // 客户端存入Cookie持久化,服务端仅当前请求使用
    if (typeof window !== "undefined") {
      Cookies.set("DEVICE", deviceToken, { expires: 365 });
    }
  }

  // 优先使用AUTH_TOKEN,其次用DEVICE_TOKEN
  const token = authToken || deviceToken;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

// 响应拦截器:处理401等错误
axiosInstance.interceptors.response.use(
  (res) => res,
  async (error) => {
    // 401时清除失效的AUTH_TOKEN,自动切换回DEVICE_TOKEN
    if (error.response?.status === 401 && typeof window !== "undefined") {
      Cookies.remove("AUTH");
      // 可选:触发TanStack Query重试请求
      error.config._retry = true;
      return axiosInstance(error.config);
    }
    return Promise.reject(error);
  }
);

export default axiosInstance;

二、NextJS + TanStack Query 处理JWT鉴权的最佳实践

1. 鉴权凭证的环境隔离管理

  • 客户端:用js-cookie存储AUTH_TOKEN和DEVICE_TOKEN,支持页面刷新后保留凭证
  • 服务端:用next/headers读取Cookie,适配SSR/SSG场景的服务端请求
  • 禁止在客户端代码中直接导入服务端专属API(如next/headers),通过环境判断或动态导入拆分逻辑

2. Axios与TanStack Query的集成

  • 直接在useQuery/useMutation中使用兼容双环境的axios实例:
    import { useQuery } from "@tanstack/react-query";
    import axiosInstance from "@/utils/axiosInstance";
    
    export const useUserInfo = () => {
      return useQuery({
        queryKey: ["userInfo"],
        queryFn: async () => {
          const res = await axiosInstance.get("/user/info");
          return res.data;
        },
      });
    };
    
  • 配置全局Query客户端,添加401重试逻辑:
    // app/providers.tsx
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: (failureCount, error) => {
            // 401错误时重试1次(配合拦截器清除凭证)
            return error.response?.status === 401 && failureCount < 1;
          },
        },
      },
    });
    
    export default function Providers({ children }) {
      return (
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      );
    }
    

3. 登录/登出的状态同步

  • 登录成功后,将AUTH_TOKEN存入Cookie,并invalidate相关查询:
    import { useMutation, useQueryClient } from "@tanstack/react-query";
    import axiosInstance from "@/utils/axiosInstance";
    import Cookies from "js-cookie";
    
    export const useLogin = () => {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: async (credentials) => {
          const res = await axiosInstance.post("/auth/login", credentials);
          return res.data;
        },
        onSuccess: (data) => {
          // 存入AUTH_TOKEN
          Cookies.set("AUTH", data.accessToken, { expires: 7 });
          // 刷新用户信息等相关查询
          queryClient.invalidateQueries({ queryKey: ["userInfo"] });
        },
      });
    };
    
  • 登出时清除AUTH_TOKEN,并invalidate所有需要鉴权的查询:
    export const useLogout = () => {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: async () => {
          await axiosInstance.post("/auth/logout");
        },
        onSuccess: () => {
          Cookies.remove("AUTH");
          queryClient.invalidateQueries();
        },
      });
    };
    

4. 服务端渲染(SSR)的凭证适配

在getServerSideProps或Server Component中,axios实例会自动读取服务端Cookie,无需额外配置:

// pages/user.tsx
import { useUserInfo } from "@/hooks/useUserInfo";
import { getServerSideProps } from "next";
import { dehydrate } from "@tanstack/react-query";
import { queryClient } from "@/app/providers";

export const getServerSideProps = async () => {
  // 服务端预获取数据,自动使用服务端Cookie的鉴权凭证
  await queryClient.prefetchQuery({
    queryKey: ["userInfo"],
    queryFn: async () => {
      const res = await axiosInstance.get("/user/info");
      return res.data;
    },
  });

  return { props: { dehydratedState: dehydrate(queryClient) } };
};

5. 安全最佳实践

  • AUTH_TOKEN建议由后端设置为HttpOnly、Secure、SameSite=Strict的Cookie,防止XSS和CSRF攻击
  • DEVICE_TOKEN作为匿名凭证,设置较长有效期,减少重复请求
  • 避免在前端存储敏感凭证,所有鉴权逻辑通过Cookie或请求头传递

内容的提问来源于stack exchange,提问作者Amir Rezvani

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 18:35:02