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

Next Auth v5实现API返回401时自动登出并刷新页面

问题:Next 14 + Next Auth v5 处理API 401自动登出并刷新页面

我正在使用Next 14和Next Auth v5的Credentials Provider做身份认证,想要实现:当后端API返回401错误时,自动在后台登出用户并刷新页面,同时重新获取所有数据。


现有代码结构

1. 统一API调用工具文件 Apiclient.js

import { getSession } from "@/lib/utils";

async function apiClient({
  path,
  options = {},
  headers = {},
  tags,
  cache = "force-cache",
}) {
  const session = await getSession();
  const token = session?.user?.accessToken;
  console.log(token);
  try {
    const mergedOptions = {
      headers: {
        "Content-Type": "application/json",
        ...(token && { Authorization: `Bearer ${token}` }),
        isGuest: token ? false : true,
        ...headers,
      },
      ...options,
      cache: cache,
      ...(tags && { next: { tags: [tags] } }),
    };

    const response = await fetch(path, mergedOptions);
  
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

export default apiClient;

2. 服务器组件中使用API客户端示例(Header组件)

"use server";
import Link from "next/link";
import { cookies } from "next/headers";
import {
  NavBar,
  Logo,
  UserDropdown,
  NavMobile,
  CategoryMenu,
} from "@/components/common";
import { Search, LocaleSwitcher } from "@/components/ui";
import { CartIcon, MiniCart } from "@/components/cart";
import { getUserSessionServer } from "@/lib/utils/getUserSessionServer";
import { getCart } from "@/framework/server-actions/cart";
import { getNavigation } from "@/framework/server-actions/menu";
import {
  WishlistProducts,
  RefreshUserSession,
  HeaderWrapper,
} from "@/components/common";

const Header = async () => {
  const cartId = cookies().get("cartId");
  const sessionPromise = await getUserSessionServer();
  const cartPromise = cartId && (await getCart());
  const menuPromise = await getNavigation();

  const [session, cart, menu] = await Promise.all([
    sessionPromise,
    cartPromise,
    menuPromise,
  ]);

  return (
    <HeaderWrapper>
      <div className="md:relative">
        <div className="md:max-w-screen-xl mx-auto">
          <div className="flex items-center justify-between">
            <div className="hidden sm:block">
              <ul className="space-x-8 text-white text-sm sm:flex rtl:space-x-reverse">
                <li>
                  <CategoryMenu links={menu} />
                </li>
                <li>
                  <Link href="/cms/about-us">About</Link>
                </li>
                <li>
                  <Link href="/contact-us">Contact</Link>
                </li>
              </ul>
            </div>
            <div className="md:absolute md:start-1/2 md:-translate-x-1/2 rtl:md:translate-x-1/2 top-0">
              <Logo />
            </div>
            <div className="">
              <div className="flex items-center">
                <Search />
                <LocaleSwitcher />
                <CartIcon cartTotalsInfo={cart} />
                <UserDropdown session={session} />
                <NavMobile links={menu} />
              </div>
            </div>
          </div>
        </div>
      </div>

      <MiniCart
        session={session}
        cartTotalsInfo={cart}
        WishlistProducts={
          <WishlistProducts
            session={session}
            className="embla__product__minicart embla__product__container__minicart"
          />
        }
      />
      <RefreshUserSession />
    </HeaderWrapper>
  );
};

export default Header;

3. 已尝试的修改

在apiClient中添加401判断:

if (response.status === 401 || response.status === 403) {
      console.log("response.status", response.status);
      return "unauthorized";
    }

在客户端组件CartIcon中处理登出:

"use client";
import { useContext, useCallback } from "react";
import { usePathname } from "next/navigation";
import { Link } from "@/i18n/routing";
import { IconBagWhite } from "@/assets";
import { CartContext } from "@/contexts/CartContext";
import { logout } from "@/framework/server-actions/account/access";

const CartIcon = ({ cartTotalsInfo }) => {
  const isCheckout = usePathname() === "/checkout";
  const { setShowMiniCart } = useContext(CartContext);
  const { itemsQuantity } = cartTotalsInfo || {};

  const handleOpenMiniCart = useCallback(
    () => setShowMiniCart(true),
    [setShowMiniCart]
  );

  if (cartTotalsInfo === "unauthorized") {
    console.log("cartTotalsInfo is", cartTotalsInfo);
    logout();
    console.log("logged out, router refresh", cartTotalsInfo);
  }

  return (
    <>
      {isCheckout ? (
        <Link href="/cart" className="block relative">
          <IconBagWhite />
          {itemsQuantity > 0 && (
            <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
              <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                {itemsQuantity}
              </div>
            </div>
          )}
        </Link>
      ) : (
        <>
          <button
            className="relative items-center px-2 hidden sm:flex"
            onClick={handleOpenMiniCart}
          >
            <IconBagWhite />
            {itemsQuantity > 0 && (
              <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
                <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                  {itemsQuantity}
                </div>
              </div>
            )}
          </button>
          <Link href="/cart" className="block relative sm:hidden">
            <IconBagWhite />
            {itemsQuantity > 0 && (
              <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
                <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                  {itemsQuantity}
                </div>
              </div>
            )}
          </Link>
        </>
      )}
    </>
  );
};

export default CartIcon;

解决方案

第一步:完善apiClient.js的错误处理逻辑

将401/403错误改为抛出异常,而非返回字符串,确保所有API调用能统一捕获:

import { getSession } from "@/lib/utils";

async function apiClient({
  path,
  options = {},
  headers = {},
  tags,
  cache = "force-cache",
}) {
  const session = await getSession();
  const token = session?.user?.accessToken;
  try {
    const mergedOptions = {
      headers: {
        "Content-Type": "application/json",
        ...(token && { Authorization: `Bearer ${token}` }),
        isGuest: token ? false : true,
        ...headers,
      },
      ...options,
      cache: cache,
      ...(tags && { next: { tags: [tags] } }),
    };

    const response = await fetch(path, mergedOptions);
  
    // 捕获401/403状态,抛出特定标识的异常
    if (response.status === 401 || response.status === 403) {
      throw new Error("UNAUTHORIZED");
    }

    const data = await response.json();
    return data;
  } catch (error) {
    // 仅透传未授权异常,其他异常正常抛出
    if (error.message === "UNAUTHORIZED") {
      throw error;
    }
    console.error(error);
    throw error;
  }
}

export default apiClient;

第二步:在服务器操作中处理登出并触发缓存更新

以getCart为例,在服务器端捕获未授权异常,执行登出并刷新缓存:

"use server";
import apiClient from "@/path/to/Apiclient";
import { logout } from "@/framework/server-actions/account/access";
import { revalidatePath } from "next/cache";

export async function getCart() {
  try {
    return await apiClient({ path: "/api/cart" });
  } catch (error) {
    if (error.message === "UNAUTHORIZED") {
      // 服务器端执行登出逻辑
      await logout();
      // 清除当前路径缓存,确保刷新后获取未登录状态
      revalidatePath("/");
      // 返回错误标识给客户端
      return { error: "UNAUTHORIZED" };
    }
    throw error;
  }
}

第三步:客户端组件触发页面刷新

修改CartIcon组件,监听未授权标识并调用页面刷新:

"use client";
import { useContext, useCallback, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { Link } from "@/i18n/routing";
import { IconBagWhite } from "@/assets";
import { CartContext } from "@/contexts/CartContext";

const CartIcon = ({ cartTotalsInfo }) => {
  const isCheckout = usePathname() === "/checkout";
  const { setShowMiniCart } = useContext(CartContext);
  const router = useRouter();
  const { itemsQuantity } = cartTotalsInfo || {};

  const handleOpenMiniCart = useCallback(
    () => setShowMiniCart(true),
    [setShowMiniCart]
  );

  // 监听未授权状态,触发页面刷新
  useEffect(() => {
    if (cartTotalsInfo?.error === "UNAUTHORIZED") {
      router.refresh();
    }
  }, [cartTotalsInfo, router]);

  return (
    <>
      {isCheckout ? (
        <Link href="/cart" className="block relative">
          <IconBagWhite />
          {itemsQuantity > 0 && (
            <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
              <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                {itemsQuantity}
              </div>
            </div>
          )}
        </Link>
      ) : (
        <>
          <button
            className="relative items-center px-2 hidden sm:flex"
            onClick={handleOpenMiniCart}
          >
            <IconBagWhite />
            {itemsQuantity > 0 && (
              <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
                <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                  {itemsQuantity}
                </div>
              </div>
            )}
          </button>
          <Link href="/cart" className="block relative sm:hidden">
            <IconBagWhite />
            {itemsQuantity > 0 && (
              <div className="absolute -top-2 h-[20px] w-[20px] rounded-full bg-red-600 -end-1 ">
                <div className="flex h-full items-center justify-center text-[11px] font-bold text-white">
                  {itemsQuantity}
                </div>
              </div>
            )}
          </Link>
        </>
      )}
    </>
  );
};

export default CartIcon;

关键说明

  1. 服务器端登出后,通过revalidatePath清除缓存,确保页面刷新时获取最新的未登录状态
  2. 客户端用router.refresh()触发页面全量刷新,重新拉取所有服务器组件数据
  3. 错误处理形成完整链路:API工具层抛出异常 → 服务器操作层处理登出 → 客户端组件触发刷新,避免分散处理

内容的提问来源于stack exchange,提问作者H.Husain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 13:44:53