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

NextJS中Refresh Token Cookie无法与浏览器同步问题求助

解决Next.js 16 Server Component中刷新Token后HttpOnly Cookie无法同步到浏览器的问题

我明白你遇到的痛点——在Server Component里预取数据时触发Token刷新,服务器端拿到了新的Cookie,但浏览器完全没同步到。核心问题其实是HttpOnly Cookie只能通过响应头的Set-Cookie字段更新,而你当前的刷新逻辑是在服务器内部完成的,新Cookie的Set-Cookie头根本没传到浏览器。下面给你两种针对性的解决方案,你可以根据业务场景选择:


方案一:保留Server Component预取,在页面响应头中传递新Cookie

这个方案适合需要在服务器端渲染用户信息的场景,核心是在刷新Token后,把新Cookie通过页面的响应头返回给浏览器,让浏览器自动更新本地Cookie。

调整后的代码实现

import { headers, cookies } from 'next/headers';
import { parse } from 'cookie'; // 确保安装了cookie库

const getMeServer = async (
  context: QueryFunctionContext,
): Promise<MeResponse> => {
  const { signal } = context;
  const { endpoint, method } = ENDPOINTS.IDENTITY_RELATED.protected.me;
  const apiInstance = axios.create({ baseURL });

  try {
    // 第一次尝试用现有Cookie请求用户信息
    const cookieStore = await cookies();
    const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE_NAMESPACE)?.value;
    const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE_NAMESPACE)?.value;
    const cookieHeader = [
      accessToken ? `${ACCESS_TOKEN_COOKIE_NAMESPACE}=${accessToken}` : "",
      refreshToken ? `${REFRESH_TOKEN_COOKIE_NAMESPACE}=${refreshToken}` : "",
    ].filter(Boolean).join("; ");

    const res = await apiInstance.request<MeResponse>({
      url: endpoint,
      method,
      signal,
      headers: { Cookie: cookieHeader },
    });
    return res.data;
  } catch (error) {
    // 捕获401错误,执行Token刷新
    if (axios.isAxiosError(error) && error.response?.status === 401) {
      const cookieStore = await cookies();
      const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE_NAMESPACE)?.value;
      if (!refreshToken) throw new Error("No refresh token available");

      // 直接调用后端刷新接口,跳过内部/api/auth/refresh路由
      const baseURL = process.env.NEXT_PUBLIC_API_URL;
      const { endpoint: refreshEndpoint, method: refreshMethod } = ENDPOINTS.IDENTITY_RELATED.public.refresh;
      const refreshRes = await axios.request({
        baseURL,
        url: refreshEndpoint,
        method: refreshMethod,
        headers: { Cookie: `${REFRESH_TOKEN_COOKIE_NAMESPACE}=${refreshToken};` },
      });

      const setCookies = refreshRes.headers["set-cookie"];
      if (setCookies) {
        const parsedCookies = parse(setCookies);
        // 更新服务器端Cookie Store
        Object.entries(parsedCookies).forEach(([name, value]) => {
          // 这里需要从Set-Cookie头中解析完整属性(比如maxAge、secure等),可以用cookie库的parse函数扩展
          cookieStore.set({
            name,
            value,
            path: "/",
            httpOnly: true,
            secure: process.env.NODE_ENV === "production",
            sameSite: "lax",
            // 从refreshRes的Set-Cookie中提取maxAge等属性
            ...(refreshRes.headers["set-cookie"].find(c => c.startsWith(name))?.split(';').reduce((acc, part) => {
              const [key, val] = part.trim().split('=');
              if (key === 'Max-Age') acc.maxAge = parseInt(val);
              if (key === 'Path') acc.path = val;
              return acc;
            }, {}))
          });
        });

        // 关键:将新Cookie的Set-Cookie头添加到页面响应中,让浏览器同步
        const responseHeaders = headers();
        setCookies.forEach(cookieStr => {
          responseHeaders.append('Set-Cookie', cookieStr);
        });
      }

      // 用新Cookie重新请求用户信息
      const newCookieStore = await cookies();
      const newAccessToken = newCookieStore.get(ACCESS_TOKEN_COOKIE_NAMESPACE)?.value;
      const newCookieHeader = newAccessToken ? `${ACCESS_TOKEN_COOKIE_NAMESPACE}=${newAccessToken}` : "";

      const res = await apiInstance.request<MeResponse>({
        url: endpoint,
        method,
        signal,
        headers: { Cookie: newCookieHeader },
      });
      return res.data;
    }
    // 其他错误直接抛出
    throw error;
  }
};

方案说明

  1. 当Server Component预取用户信息遇到401时,直接调用后端刷新接口,减少一层内部路由的调用
  2. 刷新成功后,不仅更新服务器端的cookieStore,还要把后端返回的Set-Cookie头原封不动地添加到页面的响应头中——这一步是让浏览器同步Cookie的核心
  3. 最后用新Cookie重新请求用户信息,确保预取成功

方案二:将刷新逻辑移到Client Component

如果你的业务不需要在服务器端渲染用户信息,或者可以接受用户信息在客户端加载,这个方案更简单直接。因为客户端发起的请求会直接接收后端的Set-Cookie头,浏览器会自动更新HttpOnly Cookie。

代码实现示例

// 客户端组件(必须加"use client"指令)
"use client";

import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";

// 客户端专用的Axios实例
const getClientAuthApi = () => {
  const apiInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL });

  // 配置refresh拦截器
  createAuthRefreshInterceptor(apiInstance, async (failedRequest) => {
    // 浏览器直接调用刷新接口,Set-Cookie会被自动处理
    await axios.post("/api/auth/refresh");
    
    // 从浏览器Cookie中获取新的AccessToken
    const newAccessToken = document.cookie.split('; ')
      .find(row => row.startsWith(`${ACCESS_TOKEN_COOKIE_NAMESPACE}=`))
      ?.split('=')[1];
    
    // 更新失败请求的Cookie头
    if (newAccessToken) {
      failedRequest.headers.Cookie = `${ACCESS_TOKEN_COOKIE_NAMESPACE}=${newAccessToken}`;
    }
  }, {
    statusCodes: [401],
    pauseInstanceWhileRefreshing: true,
  });

  return apiInstance;
};

// 客户端查询函数
const getMeClient = async () => {
  const api = getClientAuthApi();
  const res = await api.get(ENDPOINTS.IDENTITY_RELATED.protected.me.endpoint);
  return res.data;
};

// 渲染用户信息的组件
export function UserProfile() {
  const { data: user, isLoading } = useQuery({
    queryKey: queryAuthKeys.me(),
    queryFn: getMeClient,
    staleTime: Infinity,
  });

  if (isLoading) return <div>Loading user info...</div>;
  if (!user) return <div>Please log in</div>;

  return <div>Welcome back, {user.username}!</div>;
}

方案说明

  1. 所有和Token相关的逻辑都在客户端完成,刷新请求是浏览器发起的,后端返回的Set-Cookie会直接被浏览器处理
  2. 不需要再处理服务器端和浏览器端Cookie同步的问题,逻辑更简洁
  3. 在Server Component中直接导入这个Client Component即可,服务器端渲染页面框架,客户端加载用户信息

关键总结

HttpOnly Cookie的更新完全依赖浏览器接收响应头中的Set-Cookie字段,服务器端的cookieStore只是当前请求上下文的副本,和浏览器端Cookie是完全独立的。你之前的问题就是因为刷新逻辑在服务器内部完成,新Cookie的Set-Cookie头没有传递给浏览器——只要解决这个点,问题就迎刃而解了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 10:14:07