如何在Next.js中结合useSWR与Axios拦截器刷新JWT令牌?
从Axios切换到useSWR的JWT认证适配问题
场景与现有Axios实现
Next.js项目采用JWT认证:
- Refresh Token存储在客户端HttpOnly Cookie中
- Access Token存储在客户端内存中
现有Axios通过拦截器实现自动刷新token逻辑:
- 请求拦截器自动为私有请求添加
Authorization头部 - 响应拦截器捕获403状态时,用Refresh Token刷新Access Token,标记请求
sent避免无限循环,刷新成功后重试原请求
对应的实现钩子:
import { useEffect } from "react"; import { AxiosRequestConfig, AxiosResponse } from "axios"; import { useRefreshToken } from "./useRefreshToken"; import { privateAxios, publicAxios } from "@/api/axios"; import { useAccessTokenState } from "@/store/memory"; export const usePrivateAxios = () => { const refresh = useRefreshToken(); const accessToken = useAccessTokenState(); useEffect(() => { // Append the access token to the request const requestInterceptor = privateAxios.interceptors.request.use( (config: AxiosRequestConfig) => { config.headers = config.headers ?? {}; if (!config.headers["Authorization"]) { config.headers["Authorization"] = `Bearer ${accessToken.value}`; } return config; }, (error) => Promise.reject(error) ); // Refresh the access token if it has expired, and retry the original request const responseInterceptor = privateAxios.interceptors.response.use( (response: AxiosResponse) => response, async (error) => { const prevRequest = error?.config; if (error?.response?.status === 403 && !prevRequest?.sent) { prevRequest.sent = true; const newAccessToken = await refresh(); prevRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; return privateAxios(prevRequest); } return Promise.reject(error); } ); return () => { privateAxios.interceptors.request.eject(requestInterceptor); privateAxios.interceptors.response.eject(responseInterceptor); }; }, [accessToken, refresh]); return privateAxios; };
问题
如何用useSWR实现上述认证逻辑?直接以下方式使用是否可行?
const { data, error, isLoading } = useSWR("/api/users", usePrivateAxios());
目前无报错,但这样传入自定义钩子是否合理?刷新失败时useSWR会有怎样的表现?还有哪些未考虑的问题?
解答
1. 直接传入usePrivateAxios()不合理
每次组件渲染都会调用usePrivateAxios(),导致:
- Axios实例的拦截器被反复添加/移除,可能引发内存泄漏
- 多组件使用时,拦截器逻辑会互相干扰(比如某组件卸载时移除了拦截器,其他组件的请求丢失认证头部)
2. 正确的实现方式:封装带认证逻辑的SWR钩子
应该把认证逻辑与SWR的fetcher函数结合,而非直接传递Axios实例。示例如下:
import useSWR from 'swr'; import { useRefreshToken } from './useRefreshToken'; import { privateAxios } from '@/api/axios'; import { useAccessTokenState } from '@/store/memory'; // 全局锁:避免并发请求触发多次token刷新 let isRefreshing = false; let refreshPromise: Promise<string> | null = null; export const usePrivateSWR = (url: string) => { const refresh = useRefreshToken(); const accessToken = useAccessTokenState(); const fetcher = async (fetchUrl: string) => { try { // 设置当前有效的access token privateAxios.defaults.headers.common['Authorization'] = `Bearer ${accessToken.value}`; const response = await privateAxios.get(fetchUrl); return response.data; } catch (error: any) { // 捕获403,尝试刷新token if (error.response?.status === 403) { // 加锁:避免并发请求重复刷新 if (!isRefreshing) { isRefreshing = true; refreshPromise = refresh(); } const newAccessToken = await refreshPromise; isRefreshing = false; refreshPromise = null; // 更新内存中的access token accessToken.setValue(newAccessToken); // 重试请求 privateAxios.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; const retryResponse = await privateAxios.get(fetchUrl); return retryResponse.data; } // 其他错误直接抛出,交给SWR处理 throw error; } }; return useSWR(url, fetcher, { // 配置重试逻辑:刷新失败后停止自动重试,避免无限循环 retry: (failureCount, error) => { if (error.response?.status === 403 || error.response?.status === 401) { return false; } return failureCount < 3; }, // 可选:token更新时自动重新验证缓存 revalidateOnFocus: true, }); };
3. 刷新失败时的useSWR表现
- 如果Refresh Token失效(返回401),错误会被抛出,SWR进入
error状态 - 若未配置
retry逻辑,SWR会默认重试请求,导致无限循环请求401/403,因此必须在retry配置中过滤这类状态码 - 你可以在组件中监听
error状态,触发登录页跳转、清除用户状态等操作
4. 未考虑的关键问题
- 并发请求的重复刷新:多个请求同时触发403时,会重复调用refresh接口,需要全局锁避免这个问题(如上述示例中的
isRefreshing和refreshPromise) - 缓存同步:Access Token刷新后,SWR缓存中的旧数据可能已过期,需要调用
mutate手动更新相关缓存,或者开启revalidateOnFocus等配置自动验证 - 全局错误处理:刷新失败需要统一处理(如跳转到登录页),建议在fetcher中捕获刷新失败的错误,触发全局状态变更或路由跳转,而非每个组件单独处理
- 拦截器全局配置:如果要保留Axios拦截器逻辑,应将其配置在全局(而非组件钩子中),避免重复挂载/卸载
内容的提问来源于stack exchange,提问作者Cytronic
相关产品推荐
相关产品推荐

