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
相关产品推荐
相关产品推荐

