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

Next.js13+Firebase Auth:服务端刷新过期ID Token方案咨询

问题分析与解决方案

问题背景

基于Next.js 13和Firebase Auth(使用ID Token)开发应用时,希望借助服务端组件快速获取用户数据,因此在初始请求时通过服务端验证ID Token,未登录用户访问受保护路由会被重定向到登录页。

但遇到核心问题:用户超过1小时未操作导致ID Token过期,下一次请求携带过期Token触发auth.verifyIdToken验证失败,此时客户端的Token刷新逻辑(user.getIdToken)还未执行,就被直接重定向到登录页。想知道是否有服务端安全刷新Firebase ID Token的方案,已知Firebase REST API的workaround但安全性存疑。

上下文代码

客户端AuthContextProvider实现

const ServerAuthContextProvider = ({
  children,
  user,
  cookie,
}: {
  children: ReactNode;
  user: UserRecord;
  cookie: Cookie;
}) => {
  useEffect(() => {
    if (typeof window !== "undefined") {
      (window as any).cookie = cookie;
    }
    return auth.onIdTokenChanged(async (snap) => {
      if (!snap) {
        cookie.remove("__session");
        cookie.set("__session", "", { path: "/" });
        return;
      }
      const token = await snap.getIdToken();
      cookie.remove("__session");
      cookie.set("__session", token, { path: "/" });
    });
  }, [cookie]);

  return (
    <serverAuthContext.Provider
      value={{
        user,
        auth,
      }}
    >
      {children}
    </serverAuthContext.Provider>
  );
};

服务端根组件

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
  const { user } = await verifyAuthToken();
  if (!user) redirect("/login");

  return (
    <html lang="en">
      <body>
        <ServerAuthContextProvider user={user}>
          {children}
        </ServerAuthContextProvider>
      </body>
    </html>
  );
};

服务端Token验证逻辑

const verifyAuthToken = async () => {
  const auth = getAuth(firebaseAdmin);

  try {
    const session = cookies().get("__session");
    if (session?.value) {
      console.log("found token");
      const token = await auth.verifyIdToken(session.value);
      const { uid } = token;
      console.log("uid found: ", uid);
      const user = await auth.getUser(uid);
      return {
        auth,
        user,
      };
    }
  } catch (error: unknown) {
    if (typeof error === "string") {
      console.log("error", error);
      return {
        auth,
        error,
      };
    } else if (error instanceof Error) {
      console.log("error", error.message);

      return {
        auth,
        error: error.message,
      };
    }
  }
  return {
    auth,
  };
};

可行解决方案

首先明确:Firebase官方不支持服务端直接使用Refresh Token刷新ID Token——Refresh Token属于客户端敏感凭证,存储或传递到服务端会带来泄露风险。以下是两个安全且适配Next.js 13服务端组件的方案:

方案一:服务端识别过期Token,交由客户端刷新后重试

核心思路是服务端不直接重定向,而是返回Token过期状态,让客户端完成刷新后重新请求页面。

  1. 更新服务端验证逻辑,区分Token过期错误:
const verifyAuthToken = async () => {
  const auth = getAuth(firebaseAdmin);

  try {
    const session = cookies().get("__session");
    if (session?.value) {
      const token = await auth.verifyIdToken(session.value);
      const { uid } = token;
      const user = await auth.getUser(uid);
      return {
        auth,
        user,
        tokenExpired: false
      };
    }
  } catch (error: unknown) {
    // 识别Firebase抛出的Token过期错误
    if (error instanceof Error && (error as any).code === "auth/id-token-expired") {
      return {
        auth,
        tokenExpired: true,
        error: "Token expired"
      };
    }
    // 其他无效Token错误按原逻辑处理
    return {
      auth,
      error: error instanceof Error ? error.message : "Unknown error"
    };
  }
  return {
    auth,
    tokenExpired: false
  };
};
  1. 修改根组件,根据状态决定渲染逻辑:
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
  const { user, tokenExpired } = await verifyAuthToken();
  
  if (user) {
    return (
      <html lang="en">
        <body>
          <ServerAuthContextProvider user={user}>
            {children}
          </ServerAuthContextProvider>
        </body>
      </html>
    );
  }

  if (tokenExpired) {
    // 渲染客户端组件处理Token刷新
    return (
      <html lang="en">
        <body>
          <ClientTokenRefresher />
        </body>
      </html>
    );
  }

  // 无有效Token时重定向登录页
  redirect("/login");
};
  1. 实现客户端刷新组件:
"use client";

import { useEffect } from "react";
import { auth } from "@/firebase/client";
import { useRouter } from "next/navigation";

const ClientTokenRefresher = () => {
  const router = useRouter();

  useEffect(() => {
    const refreshToken = async () => {
      try {
        const user = auth.currentUser;
        if (user) {
          // 强制刷新ID Token
          const newToken = await user.getIdToken(true);
          // 更新Cookie
          document.cookie = `__session=${newToken}; path=/; secure; sameSite=strict`;
          // 重新加载当前页面
          router.refresh();
        } else {
          router.push("/login");
        }
      } catch (error) {
        // 刷新失败(如Refresh Token过期)则跳转登录
        router.push("/login");
      }
    };

    refreshToken();
  }, [router]);

  return <div>正在重新验证身份...</div>;
};

export default ClientTokenRefresher;

方案二:客户端提前刷新Token,避免过期Token发送到服务端

通过提前检测Token过期时间,在Token即将失效时主动刷新并更新Cookie,从根源减少过期Token被服务端接收的概率。

修改ServerAuthContextProvider组件:

"use client";

import { useEffect } from "react";
import { auth } from "@/firebase/client";

const ServerAuthContextProvider = ({
  children,
  user,
  cookie,
}: {
  children: ReactNode;
  user: UserRecord;
  cookie: Cookie;
}) => {
  useEffect(() => {
    const setupTokenRefresh = async () => {
      const user = auth.currentUser;
      if (!user) return;

      // 解析当前Token的过期时间
      const token = await user.getIdToken();
      const decoded = JSON.parse(atob(token.split(".")[1]));
      const expiresIn = decoded.exp * 1000 - Date.now();
      
      // 提前5分钟刷新Token
      const refreshTimeout = setTimeout(async () => {
        const newToken = await user.getIdToken(true);
        cookie.set("__session", newToken, { path: "/" });
      }, expiresIn - 5 * 60 * 1000);

      // 监听Token变化,实时更新Cookie
      const unsubscribe = auth.onIdTokenChanged(async (snap) => {
        if (!snap) {
          cookie.remove("__session");
          cookie.set("__session", "", { path: "/" });
          return;
        }
        const newToken = await snap.getIdToken();
        cookie.set("__session", newToken, { path: "/" });
      });

      return () => {
        clearTimeout(refreshTimeout);
        unsubscribe();
      };
    };

    setupTokenRefresh();
  }, [cookie]);

  return (
    <serverAuthContext.Provider
      value={{
        user,
        auth,
      }}
    >
      {children}
    </serverAuthContext.Provider>
  );
};

关键注意事项

  • 绝对不要在服务端存储或传递Refresh Token,这会导致账号被盗的严重风险。
  • 方案一兼顾了服务端组件的性能优势和客户端Token刷新的安全性,是最稳妥的适配方案。
  • 方案二能提升用户体验,但依赖客户端持续运行,若用户关闭页面后再打开仍可能触发过期问题,可与方案一结合使用。

内容的提问来源于stack exchange,提问作者flair

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 20:00:16