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

Next.js页面闲置时刷新Cookie值变为null的问题

Next.js页面闲置时刷新Cookie值变为null的问题

大伙好,我最近碰到个挺闹心的问题——我把访问令牌和刷新令牌分别存在不同的Cookie里,两个Cookie都设置成7天后过期。访问令牌本身是每10分钟就过期,我还特意做了个刷新令牌的函数,本想着在访问令牌过期前1分钟自动更新状态,活跃用网站的时候一切都正常,但只要闲置一段时间,再回到页面就发现访问令牌的值变成null了,这可把我整懵了。

下面是我当前的AuthContext代码,麻烦大伙帮我看看哪里出问题了:

"use client";
import { createContext, useContext, useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import jwt from "jsonwebtoken";
import { useRouter } from "next/navigation";

const AuthContext = createContext();

export const AuthProvider = ({ 
children
 }) => {
  const apiURL = process.env.NEXT_PUBLIC_API_URL;
  const router = useRouter();

  const [user, setUser] = useState(null);
  const [token, setToken] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const storedToken = Cookies.get("token");
    if (storedToken) {
      setToken(storedToken);
    } else {
      logoutAction();
    }
  }, []);

  useEffect(() => {
    const storedUser = JSON.parse(localStorage.getItem("user"));

    if (storedUser) {
      if (storedUser.roleTypeId === 3) {
        router.push("/dashboard");
      }
    }
  }, [user]);

  useEffect(() => {
    const initializeAuth = async () => {
      setLoading(true);
      console.log(token);
      try {
        const storedUser = localStorage.getItem("user");
        if (storedUser) setUser(JSON.parse(storedUser));

        const currentToken = Cookies.get("token");
        console.log(currentToken);
        if (currentToken) {
          const decodedToken = jwt.decode(currentToken);
          if (Date.now() >= decodedToken?.exp * 1000) {
            await handleTokenRefresh();
          }
        } else {
          logoutAction();
        }
      } catch (error) {
        console.error("Error during auth initialization:", error);
      } finally {
        setLoading(false);
      }
    };

    initializeAuth();
  }, [token]);

  useEffect(() => {
    const intervalId = setInterval(async () => {
      try {
        if (token) {
          const decodedToken = jwt.decode(token);

          if (Date.now() >= decodedToken?.exp * 1000) {
            await handleTokenRefresh();
          }
        }
      } catch (error) {
        throw error;
      }
    }, 60000);

    return () => {
      clearInterval(intervalId);
    };
  });

  const handleTokenRefresh = async () => {
    try {
      const currentRefreshToken = Cookies.get("refreshToken");
      const currentToken = Cookies.get("token");

      const { data } = await axios.post(
        `${apiURL}/Authentication/RefreshToken`,
        {
          refreshToken: currentRefreshToken,
          token: currentToken,
        },
        {
          headers: {
            "Content-Type": "application/json",
            Accept: "application/json",
            Authorization: `bearer ${token}`,
          },
        }
      );

      updateTokens(data.token, data.refreshToken);
      return data;
    } catch (error) {
      console.error("Error refreshing token:", error);
      logoutAction();
      throw error;
    }
  };

  const updateTokens = (
newToken
, newRefreshToken) => {
    const cookieOptions = {
      secure: true,
      sameSite: "strict",
      expires: 7,
      path: "/",
      domain: window.location.hostname,
    };

    Cookies.set("token", 
newToken
, cookieOptions);
    Cookies.set("refreshToken", 
newRefreshToken
, cookieOptions);
    setToken(
newToken
);
  };

  const LoginAction = async (
loginData
) => {
    setLoading(true);
    try {
      const { data } = await axios.post(
        `${apiURL}/Authentication/LoginUser`,
        
loginData
,
        {
          headers: {
            "Content-Type": "application/json",
            Accept: "application/json",
          },
        }
      );

      updateTokens(
        data.authenticationResult.token,
        data.authenticationResult.refreshToken
      );

      localStorage.setItem("user", JSON.stringify(data.user));
      setUser(data.user);
      return data;
    } catch (error) {
      throw error.response;
    } finally {
      setLoading(false);
    }
  };

  const logoutAction = () => {
    Cookies.remove("token");
    Cookies.remove("refreshToken");
    localStorage.removeItem("user");
    setUser(null);
    setToken(null);
    router.push("/");
  };

  return (
    <AuthContext.Provider 
value
={{ LoginAction, logoutAction, user, loading }}>
      {
children
}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

我琢磨出来的问题点和修复建议

自己捣鼓了一阵,大概找到几个可能导致这个问题的地方,给你针对性的修复思路:

  1. 固定轮询定时器不精准,后台页面被节流
    当前你用每分钟固定轮询的方式检查Token,但浏览器对后台标签的定时器会做节流,闲置时可能没及时触发刷新。改成根据Token过期时间计算延迟,用setTimeout代替setInterval更靠谱:
useEffect(() => {
  let refreshTimer;

  if (token) {
    const decodedToken = jwt.decode(token);
    if (!decodedToken) return;

    // 提前1分钟触发刷新
    const timeUntilExpire = decodedToken.exp * 1000 - Date.now();
    const refreshDelay = Math.max(timeUntilExpire - 60 * 1000, 0);

    refreshTimer = setTimeout(async () => {
      await handleTokenRefresh();
    }, refreshDelay);
  }

  return () => clearTimeout(refreshTimer);
}, [token]);
  1. Cookie的domain设置导致无法持久化
    你设置了domain: window.location.hostname,在localhost本地开发或者跨子域名场景下,这个设置会让Cookie无法正确保存,闲置后被浏览器清掉。改成只在生产环境配置明确的域名:
const cookieOptions = {
  secure: process.env.NODE_ENV === "production", // 生产环境再开secure
  sameSite: "strict",
  expires: 7,
  path: "/",
  ...(process.env.NODE_ENV === "production" && process.env.NEXT_PUBLIC_COOKIE_DOMAIN 
    ? { domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN } 
    : {})
};
  1. 刷新Token时带了过期的Authorization头
    刷新Token的接口应该只需要请求体里的refreshToken和旧Token,不需要带Authorization头(毕竟旧Token可能已经过期了),去掉这个头避免请求失败:
const handleTokenRefresh = async () => {
  try {
    const currentRefreshToken = Cookies.get("refreshToken");
    const currentToken = Cookies.get("token");

    const { data } = await axios.post(
      `${apiURL}/Authentication/RefreshToken`,
      { refreshToken: currentRefreshToken, token: currentToken },
      { headers: { "Content-Type": "application/json", Accept: "application/json" } }
    );

    updateTokens(data.token, data.refreshToken);
    return data;
  } catch (error) {
    console.error("刷新Token失败:", error);
    logoutAction();
    throw error;
  }
};
  1. 多useEffect初始化逻辑有竞态
    你有三个处理Auth初始化的useEffect,可能导致状态更新冲突。合并成一个初始化函数,确保逻辑顺序正确:
useEffect(() => {
  const initializeAuth = async () => {
    setLoading(true);
    try {
      const storedToken = Cookies.get("token");
      const storedUser = localStorage.getItem("user");

      if (storedToken) {
        setToken(storedToken);
        const decodedToken = jwt.decode(storedToken);
        if (Date.now() >= decodedToken?.exp * 1000) {
          await handleTokenRefresh();
        }
      } else {
        logoutAction();
      }

      if (storedUser) {
        const parsedUser = JSON.parse(storedUser);
        setUser(parsedUser);
        if (parsedUser.roleTypeId === 3) router.push("/dashboard");
      }
    } catch (error) {
      console.error("初始化Auth出错:", error);
      logoutAction();
    } finally {
      setLoading(false);
    }
  };

  initializeAuth();
}, []);
  1. 监听页面从后台激活的事件
    加上页面可见性变化的监听,当用户从后台切回页面时,立即检查Token状态,必要时刷新:
useEffect(() => {
  const handlePageVisible = async () => {
    if (document.visibilityState === "visible" && token) {
      const decodedToken = jwt.decode(token);
      if (Date.now() >= decodedToken?.exp * 1000) {
        await handleTokenRefresh();
      }
    }
  };

  document.addEventListener("visibilitychange", handlePageVisible);
  return () => document.removeEventListener("visibilitychange", handlePageVisible);
}, [token]);

按照这些建议改完后,应该就能解决闲置时Token变成null的问题了,你可以一步步试试~

备注:内容来源于stack exchange,提问作者Mostafa Ehab

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 13:13:11