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

Next.js 13.4中SSR组件无法更新HTTP-only Cookie的问题

NestJS + NextJS SSR中HTTP-only Cookie刷新令牌的解决方案

问题根源

SSR请求在Next.js服务器端发起,Axios拦截器运行在服务器端时,刷新令牌后服务器返回的Set-Cookie头只会作用于服务器端的请求上下文,无法直接同步到客户端浏览器的Cookie中。而客户端组件的请求在浏览器中发送,服务器返回的Set-Cookie会被浏览器自动处理,更新HTTP-only Cookie。

解决方案

方案1:将授权请求迁移到客户端(推荐)

如果业务允许,把需要授权的数据获取逻辑放到客户端组件中,使用SWR、React Query等数据获取库,让请求在浏览器环境发起。这样刷新令牌时,服务器返回的Set-Cookie头会被浏览器自动接收并更新HTTP-only Cookie。

示例代码(用SWR实现):

"use client";
import useSWR from 'swr';
import axios from 'axios';

// 配置Axios拦截器处理令牌刷新
axios.interceptors.response.use(
  (res) => res,
  async (err) => {
    const originalReq = err.config;
    if (err.response.status === 401 && !originalReq._retry) {
      originalReq._retry = true;
      // 调用刷新令牌接口
      await axios.post('/api/auth/refresh', {}, { withCredentials: true });
      // 重新发起原请求
      return axios(originalReq);
    }
    return Promise.reject(err);
  }
);

const fetcher = (url) => axios.get(url, { withCredentials: true }).then(res => res.data);

export default function UserProfile() {
  const { data: user, error } = useSWR('/api/user', fetcher);

  if (error) return <div>加载用户信息失败</div>;
  if (!user) return <div>加载中...</div>;

  return <div>欢迎回来,{user.username}</div>;
}

方案2:用Next.js Middleware统一处理令牌刷新

Middleware会在所有请求(包括SSR、客户端请求)到达页面/API路由前运行,可以在这里统一检查access token是否过期,调用刷新接口后将新的Set-Cookie头传递给客户端,确保Cookie在请求进入SSR逻辑前就已更新。

示例代码(middleware.ts):

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwtDecode from 'jwt-decode';

export async function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('access_token');
  const refreshToken = request.cookies.get('refresh_token');
  let isTokenExpired = false;

  // 解码access token判断是否过期
  if (accessToken) {
    try {
      const decoded = jwtDecode<{ exp: number }>(accessToken.value);
      isTokenExpired = Date.now() >= decoded.exp * 1000;
    } catch (err) {
      isTokenExpired = true;
    }
  }

  if (isTokenExpired && refreshToken) {
    // 调用NestJS刷新令牌接口
    const refreshRes = await fetch(`${process.env.NEST_API_BASE_URL}/auth/refresh`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Cookie: request.headers.get('Cookie') || '',
      },
      credentials: 'include',
      body: JSON.stringify({ refreshToken: refreshToken.value }),
    });

    if (refreshRes.ok) {
      // 获取新的Cookie头并设置到响应中
      const newCookies = refreshRes.headers.getSetCookie();
      const response = NextResponse.next();
      newCookies.forEach(cookie => response.headers.append('Set-Cookie', cookie));
      return response;
    } else {
      // 刷新失败,跳转到登录页
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

// 指定需要拦截的授权路由
export const config = {
  matcher: ['/profile', '/dashboard', '/api/user/:path*'],
};

方案3:在SSR数据获取逻辑中手动同步Cookie

如果必须在SSR组件中发起请求,可在getServerSideProps中处理令牌刷新,并将新的Set-Cookie头通过响应传递给客户端。

示例代码(getServerSideProps):

export async function getServerSideProps(context) {
  const apiBaseUrl = process.env.NEST_API_BASE_URL;
  const cookieHeader = context.req.headers.cookie || '';

  // 尝试获取用户信息
  let userRes = await fetch(`${apiBaseUrl}/api/user`, {
    headers: { Cookie: cookieHeader },
    credentials: 'include',
  });

  // 401表示access token过期,尝试刷新
  if (userRes.status === 401) {
    const refreshRes = await fetch(`${apiBaseUrl}/auth/refresh`, {
      method: 'POST',
      headers: {
        Cookie: cookieHeader,
        'Content-Type': 'application/json',
      },
      credentials: 'include',
      body: JSON.stringify({ refreshToken: context.req.cookies.refresh_token }),
    });

    if (refreshRes.ok) {
      // 获取新的Cookie并设置到响应头,同步给客户端
      const newCookies = refreshRes.headers.getSetCookie();
      newCookies.forEach(cookie => context.res.setHeader('Set-Cookie', cookie));

      // 用新的Cookie重新请求用户信息
      userRes = await fetch(`${apiBaseUrl}/api/user`, {
        headers: { Cookie: newCookies.map(c => c.split(';')[0]).join('; ') },
        credentials: 'include',
      });
    } else {
      // 刷新失败,跳转到登录页
      return {
        redirect: { destination: '/login', permanent: false },
      };
    }
  }

  const user = await userRes.json();
  return { props: { user } };
}

总结

  • 优先选择方案1,客户端数据获取更简单,且天然适配HTTP-only Cookie的更新逻辑。
  • 如果需要统一处理所有授权请求,方案2的Middleware是最优解,无需在每个页面重复编写刷新逻辑。
  • 方案3仅适用于必须在SSR中获取授权数据的场景,代码繁琐且维护成本高。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 17:24:59