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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 04:02:56