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;
关键说明
- 服务器端登出后,通过
revalidatePath清除缓存,确保页面刷新时获取最新的未登录状态 - 客户端用
router.refresh()触发页面全量刷新,重新拉取所有服务器组件数据 - 错误处理形成完整链路:API工具层抛出异常 → 服务器操作层处理登出 → 客户端组件触发刷新,避免分散处理
内容的提问来源于stack exchange,提问作者H.Husain
相关产品推荐
相关产品推荐

